Типы данных и коллекции
Пользовательские типы, generics, списки, словари, множества и итераторы.
- Цели главы
- Пользовательские типы
- Labelled fields
- Type aliases
- Generics
- Constants
- Кортежи
- Списки
- Модуль gleam/list
- Dict
- Set
- Queue
- Iterator
- Builder-паттерн
- Проект: модель файловой системы
- Упражнения
- Заключение
Цели главы
В этой главе мы:
- Научимся создавать пользовательские типы (custom types)
- Познакомимся с generics и type aliases
- Разберём основные коллекции: List, Dict, Set, Queue
- Изучим ленивые последовательности (Iterator)
- Познакомимся с builder-паттерном — стандартным идиомом Gleam-экосистемы
- Построим проект — модель файловой системы
Пользовательские типы
Пользовательские типы (custom types) — основной способ моделирования данных в Gleam. Они похожи на алгебраические типы данных (ADT) из Haskell и OCaml.
Типы-перечисления
Простейший вариант — перечисление:
pub type Color {
Red
Green
Blue
}
Тип Color имеет три значения: Red, Green, Blue. Используем case для работы с ними:
pub fn to_hex(color: Color) -> String {
case color {
Red -> "#FF0000"
Green -> "#00FF00"
Blue -> "#0000FF"
}
}
Функция to_hex использует case для сопоставления с образцом по значениям перечисления и возвращает соответствующий HTML-цвет.
Типы с данными
Варианты могут содержать данные:
pub type Shape {
Circle(radius: Float)
Rectangle(width: Float, height: Float)
}
Здесь Circle и Rectangle — конструкторы: функции, создающие значения типа Shape.
Конструкторы можно вызывать с метками или позиционно:
// С метками — порядок не важен
let c = Circle(radius: 5.0)
let r = Rectangle(height: 4.0, width: 3.0)
// Позиционно — порядок как в определении типа
let c = Circle(5.0)
let r = Rectangle(3.0, 4.0)
Можно комбинировать: часть аргументов позиционно, часть с метками.
Pattern matching на записях
Pattern matching извлекает данные из конструкторов:
pub fn area(shape: Shape) -> Float {
case shape {
Circle(radius:) -> 3.14159265358979 *. radius *. radius
Rectangle(width:, height:) -> width *. height
}
}
В паттерне Circle(radius:) используется сокращённый синтаксис: имя переменной совпадает с именем поля. Это эквивалентно Circle(radius: radius).
Чтобы игнорировать часть полей, используйте ..:
pub fn name(shape: Shape) -> String {
case shape {
Circle(..) -> "круг"
Rectangle(..) -> "прямоугольник"
}
}
// Извлечь только одно поле, остальные проигнорировать
case shape {
Circle(radius:, ..) -> radius
Rectangle(width:, ..) -> width
}
.. в паттерне означает «остальные поля не важны». Для отдельных полей можно использовать _:
case shape {
Rectangle(_, height) -> height // игнорируем width
Circle(radius) -> radius
}
Типы-записи
Тип с единственным вариантом работает как запись (struct/record):
pub type User {
User(name: String, email: String, age: Int)
}
let alice = User(name: "Алиса", email: "alice@example.com", age: 30)
// Доступ к полям через точку
alice.name // "Алиса"
alice.age // 30
Тип с одним вариантом работает как запись (struct): конструктор называется так же, как тип, а поля доступны через точечный синтаксис.
Доступ к полям (record accessors)
Точечный синтаксис record.field работает для типов с одним вариантом без ограничений. Для типов с несколькими вариантами поле доступно через точку только если оно имеет одинаковое имя, позицию и тип во всех вариантах:
pub type SchoolPerson {
Teacher(name: String, subject: String)
Student(name: String)
}
let t = Teacher("Иванов", "Физика")
let s = Student("Петров")
t.name // "Иванов" — поле name есть в обоих вариантах
s.name // "Петров" — ок
// t.subject — ✗ ошибка! subject есть только у Teacher
// Для доступа к таким полям используйте pattern matching
Точечный синтаксис работает для поля только тогда, когда оно присутствует во всех вариантах типа с одинаковым именем и типом; иначе необходимо использовать pattern matching.
Обновление записи
Для создания модифицированной копии записи используется синтаксис ..:
let alice = User(name: "Алиса", email: "alice@example.com", age: 30)
let older = User(..alice, age: 31)
// User(name: "Алиса", email: "alice@example.com", age: 31)
Исходная запись alice не изменяется — создаётся новая копия с обновлённым полем.
Labelled fields
Поля конструкторов в Gleam могут быть именованными (labelled). Это улучшает читаемость:
pub type Point {
Point(x: Float, y: Float)
}
// С метками — порядок не важен
let point = Point(y: 4.0, x: 3.0)
// Позиционно — порядок как в определении
let point = Point(3.0, 4.0)
// В pattern matching
case point {
Point(x:, y:) -> x +. y
}
Метки при вызове опциональны — можно передавать аргументы и позиционно.
Type aliases
Type alias — альтернативное имя для существующего типа:
pub type Name = String
pub type Age = Int
pub type Coordinate = #(Float, Float)
Alias не создаёт новый тип — это просто синоним для удобства. Name и String полностью взаимозаменяемы.
Generics
Типы могут быть параметризованы другими типами:
pub type Pair(a, b) {
Pair(first: a, second: b)
}
a и b — параметры типа. При использовании они подставляются конкретными типами:
let pair_of_ints: Pair(Int, Int) = Pair(first: 1, second: 2)
let mixed: Pair(String, Bool) = Pair(first: "hello", second: True)
Функции тоже могут быть обобщёнными:
pub fn swap(pair: Pair(a, b)) -> Pair(b, a) {
Pair(first: pair.second, second: pair.first)
}
Gleam выводит параметры типов автоматически — явно указывать их нужно редко.
Пример: тип Box
pub type Box(a) {
Box(value: a)
Empty
}
pub fn map_box(box: Box(a), f: fn(a) -> b) -> Box(b) {
case box {
Box(value:) -> Box(value: f(value))
Empty -> Empty
}
}
Box(a) — контейнер, который может содержать значение типа a или быть пустым. Функция map_box применяет функцию к содержимому, не раскрывая контейнер.
Constants
Константы объявляются на уровне модуля:
const pi = 3.14159265358979
const max_retries = 3
const greeting = "Привет!"
Константы вычисляются на этапе компиляции. В них можно использовать только литералы и другие константы — вызовы функций запрещены.
Константы можно использовать в паттернах:
const admin_email = "admin@example.com"
pub fn is_admin(email: String) -> Bool {
case email {
e if e == admin_email -> True
_ -> False
}
}
Здесь показано использование константы в охраннике (guard) паттерна: константа admin_email сравнивается со входным значением прямо в ветке case.
Кортежи
Кортежи (tuples) — упорядоченные коллекции фиксированного размера с элементами разных типов:
let point = #(3.0, 4.0) // #(Float, Float)
let person = #("Алиса", 30) // #(String, Int)
let triple = #(1, "hello", True) // #(Int, String, Bool)
Доступ к элементам — через .0, .1, .2:
point.0 // 3.0
point.1 // 4.0
person.0 // "Алиса"
Pattern matching на кортежах:
let #(x, y) = point
// x = 3.0, y = 4.0
case person {
#(name, age) if age >= 18 -> name <> " — взрослый"
#(name, _) -> name <> " — ребёнок"
}
Кортежи удобны для возврата нескольких значений из функции. Для более сложных структур лучше использовать custom types с именованными полями.
Списки
Списки — основная коллекция в Gleam. Это связные списки (linked lists): эффективное добавление/удаление в начало, но медленный доступ по индексу.
Создание списков
let empty = []
let numbers = [1, 2, 3, 4, 5]
let strings = ["привет", "мир"]
Все элементы списка должны быть одного типа.
Добавление в начало
Оператор .. добавляет элемент(ы) в начало списка:
let numbers = [1, 2, 3]
let more = [0, ..numbers] // [0, 1, 2, 3]
Это операция за O(1) — самый эффективный способ добавления.
Pattern matching на списках
pub fn describe(xs: List(Int)) -> String {
case xs {
[] -> "пустой список"
[x] -> "один элемент: " <> int.to_string(x)
[first, ..rest] -> {
"первый: " <> int.to_string(first)
<> ", остальных: " <> int.to_string(list.length(rest))
}
}
}
Паттерн [first, ..rest] разбивает список на голову (first) и хвост (rest). Паттерн [x] совпадает со списком из одного элемента.
Модуль gleam/list
Модуль gleam/list — один из самых богатых в стандартной библиотеке. Разберём основные функции по категориям.
Трансформация
import gleam/list
// map — применить функцию к каждому элементу
list.map([1, 2, 3], fn(x) { x * 2 })
// [2, 4, 6]
// filter — оставить элементы, удовлетворяющие предикату
list.filter([1, 2, 3, 4, 5], fn(x) { x % 2 == 0 })
// [2, 4]
// filter_map — map + filter за один проход
list.filter_map([1, 2, 3, 4], fn(x) {
case x % 2 == 0 {
True -> Ok(x * 10)
False -> Error(Nil)
}
})
// [20, 40]
// flat_map — map, возвращающий список, + flatten
list.flat_map([1, 2, 3], fn(x) { [x, x * 10] })
// [1, 10, 2, 20, 3, 30]
Блок демонстрирует основные операции преобразования списков: map применяет функцию к каждому элементу, filter оставляет только подходящие элементы, а flat_map сочетает отображение и выравнивание вложенных списков.
Свёртки
Свёртка (fold) — самая мощная операция на списках. Она проходит список, накапливая результат:
// fold — свёртка слева
list.fold([1, 2, 3, 4], 0, fn(acc, x) { acc + x })
// 10
// reduce — свёртка без начального значения
list.reduce([1, 2, 3], fn(acc, x) { acc + x })
// Ok(6)
// reduce на пустом списке возвращает Error(Nil)
list.reduce([], fn(acc, x) { acc + x })
// Error(Nil)
Разница между fold и reduce: fold принимает начальное значение аккумулятора, reduce использует первый элемент списка.
Поиск
// find — первый элемент, удовлетворяющий предикату
list.find([1, 2, 3, 4], fn(x) { x > 2 })
// Ok(3)
// contains — проверка наличия элемента
list.contains([1, 2, 3], 2) // True
// any / all — есть ли хотя бы один / все ли
list.any([1, 2, 3], fn(x) { x > 2 }) // True
list.all([1, 2, 3], fn(x) { x > 0 }) // True
Функции find, contains, any и all позволяют проверять наличие элементов в списке, возвращая Result или Bool в зависимости от задачи.
Сортировка
import gleam/int
// sort — сортировка с функцией сравнения
list.sort([3, 1, 4, 1, 5], int.compare)
// [1, 1, 3, 4, 5]
// unique — убрать дубликаты (сохраняя порядок)
list.unique([1, 2, 3, 2, 1, 4])
// [1, 2, 3, 4]
// reverse
list.reverse([1, 2, 3])
// [3, 2, 1]
Функции sort, unique и reverse позволяют упорядочивать список: sort требует функцию сравнения (например, int.compare), unique убирает дубликаты, сохраняя порядок первого вхождения.
Комбинирование
// append — соединить два списка
list.append([1, 2], [3, 4])
// [1, 2, 3, 4]
// flatten — развернуть список списков
list.flatten([[1, 2], [3], [4, 5]])
// [1, 2, 3, 4, 5]
// zip — попарно объединить элементы
list.zip([1, 2, 3], ["a", "b", "c"])
// [#(1, "a"), #(2, "b"), #(3, "c")]
// intersperse — вставить элемент между каждой парой
list.intersperse([1, 2, 3], 0)
// [1, 0, 2, 0, 3]
Функции комбинирования позволяют соединять несколько списков: append и flatten объединяют элементы в один список, zip создаёт пары из двух списков, а intersperse вставляет разделитель между элементами.
Разбиение
// take / drop — взять / отбросить N элементов
list.take([1, 2, 3, 4, 5], 3) // [1, 2, 3]
list.drop([1, 2, 3, 4, 5], 2) // [3, 4, 5]
// split — разделить на два списка
list.split([1, 2, 3, 4, 5], 3)
// #([1, 2, 3], [4, 5])
// partition — разделить по предикату
list.partition([1, 2, 3, 4, 5], fn(x) { x % 2 == 0 })
// #([2, 4], [1, 3, 5])
// chunk — группировка последовательных элементов
list.chunk([1, 1, 2, 2, 2, 3], fn(x) { x })
// [[1, 1], [2, 2, 2], [3]]
Функции разбиения позволяют делить список на части: take/drop отрезают N элементов, split возвращает кортеж из двух частей, partition делит по предикату, а chunk группирует подряд идущие равные элементы.
Key-value списки
Списки кортежей можно использовать как простые словари:
let entries = [#("name", "Алиса"), #("city", "Москва")]
list.key_find(entries, "name") // Ok("Алиса")
list.key_find(entries, "phone") // Error(Nil)
list.key_set(entries, "city", "Петербург")
// [#("name", "Алиса"), #("city", "Петербург")]
Функции key_find и key_set позволяют работать со списком кортежей как с ассоциативным массивом: искать значение по ключу и обновлять его, не прибегая к полноценному Dict.
group — группировка
list.group(["cat", "car", "dog", "day"], fn(w) {
string.slice(w, 0, 1)
})
// dict.from_list([
// #("c", ["cat", "car"]),
// #("d", ["dog", "day"]),
// ])
list.group возвращает Dict — словарь, который мы рассмотрим далее.
Dict
Dict(key, value) — словарь (хеш-таблица). Ключи уникальны.
import gleam/dict
// Создание
let d = dict.from_list([#("a", 1), #("b", 2), #("c", 3)])
// Вставка и получение
let d = dict.insert(d, "d", 4)
dict.get(d, "a") // Ok(1)
dict.get(d, "z") // Error(Nil)
// Проверка
dict.has_key(d, "a") // True
dict.size(d) // 4
// Удаление
let d = dict.delete(d, "b")
Блок показывает базовые операции над словарём: создание из списка пар, получение значений, проверку наличия ключа, получение размера и удаление элементов.
Трансформации Dict
// map_values — преобразовать значения
dict.map_values(d, fn(_key, value) { value * 10 })
// filter — отфильтровать пары
dict.filter(d, fn(_key, value) { value > 2 })
// merge — объединить два словаря (правый приоритетнее)
let d1 = dict.from_list([#("a", 1), #("b", 2)])
let d2 = dict.from_list([#("b", 20), #("c", 30)])
dict.merge(d1, d2)
// dict.from_list([#("a", 1), #("b", 20), #("c", 30)])
// fold — свёртка по парам ключ-значение
dict.fold(d, 0, fn(acc, _key, value) { acc + value })
// keys и values
dict.keys(d) // список ключей
dict.values(d) // список значений
// to_list — преобразование в список кортежей
dict.to_list(d)
// [#("a", 1), #("c", 3), #("d", 4)]
Модуль dict предоставляет функции для преобразования словаря: map_values и filter работают аналогично своим аналогам для списков, merge объединяет два словаря (при совпадении ключей побеждает правый), а fold позволяет свернуть все пары ключ-значение в одно значение.
upsert — вставка с обновлением
// Если ключ есть — обновить, если нет — вставить
dict.upsert(d, "a", fn(existing) {
case existing {
Some(n) -> n + 1
None -> 1
}
})
upsert удобен для подсчёта или накопления данных: функция получает Some(существующее_значение) или None, если ключа ещё нет, и возвращает новое значение.
Set
Set(a) — неупорядоченное множество уникальных элементов:
import gleam/set
let s1 = set.from_list([1, 2, 3, 4])
let s2 = set.from_list([3, 4, 5, 6])
set.contains(s1, 2) // True
set.size(s1) // 4
// Теоретико-множественные операции
set.union(s1, s2) // {1, 2, 3, 4, 5, 6}
set.intersection(s1, s2) // {3, 4}
set.difference(s1, s2) // {1, 2}
set.is_subset(
set.from_list([1, 2]),
s1,
) // True
// Добавление и удаление
let s = set.insert(s1, 5) // {1, 2, 3, 4, 5}
let s = set.delete(s1, 1) // {2, 3, 4}
Set полезен для проверки уникальности, удаления дубликатов и теоретико-множественных операций.
Queue
Queue(a) — двусторонняя очередь с эффективными операциями на обоих концах:
import gleam/queue
let q = queue.from_list([1, 2, 3])
// Добавление
let q = queue.push_back(q, 4) // [1, 2, 3, 4]
let q = queue.push_front(q, 0) // [0, 1, 2, 3, 4]
// Извлечение
queue.pop_front(q) // Ok(#(0, остаток))
queue.pop_back(q) // Ok(#(4, остаток))
// Проверки
queue.is_empty(q) // False
queue.length(q) // 5
Queue реализована на двух списках и обеспечивает амортизированное O(1) для всех операций. Она полезна, когда нужно эффективно добавлять и извлекать элементы с обоих концов.
Iterator
Iterator(a) — ленивая последовательность. В отличие от списка, элементы вычисляются по требованию:
import gleam/iterator
// Создание из списка
let it = iterator.from_list([1, 2, 3])
// Бесконечные итераторы
let ones = iterator.repeat(1) // 1, 1, 1, ...
let nats = iterator.range(1, 1000) // 1, 2, ..., 1000
// Ленивые трансформации (не вычисляются сразу!)
let doubled = iterator.map(nats, fn(x) { x * 2 })
let evens = iterator.filter(nats, fn(x) { x % 2 == 0 })
// Материализация — здесь элементы вычисляются
iterator.to_list(iterator.take(doubled, 5))
// [2, 4, 6, 8, 10]
Пример демонстрирует ленивые вычисления: итератор бесконечной последовательности натуральных чисел создаётся с помощью iterate, преобразуется через map и filter, и лишь при вызове to_list(take(...)) элементы фактически вычисляются.
unfold — создание итератора из функции
// Числа Фибоначчи
let fibs = iterator.unfold(
#(0, 1),
fn(state) {
let #(a, b) = state
iterator.Next(element: a, accumulator: #(b, a + b))
},
)
iterator.to_list(iterator.take(fibs, 8))
// [0, 1, 1, 2, 3, 5, 8, 13]
unfold принимает начальное состояние и функцию, которая на каждом шаге возвращает элемент и новое состояние. iterator.Next продолжает генерацию, iterator.Done завершает.
iterate — бесконечная итерация
// Степени двойки
let powers = iterator.iterate(1, fn(x) { x * 2 })
iterator.to_list(iterator.take(powers, 10))
// [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
iterator.iterate строит бесконечную последовательность, применяя функцию к предыдущему элементу; вместе с take это позволяет ленивo получить нужное количество значений без лишних вычислений.
Когда использовать Iterator вместо List
- Данные слишком большие, чтобы уместиться в памяти
- Нужны только первые N элементов из потенциально большой последовательности
- Вычисление каждого элемента дорогое
Builder-паттерн
Builder — один из самых распространённых паттернов в Gleam-экосистеме. Его популярность объясняется просто: в Gleam нет аргументов по умолчанию и нет перегрузки функций. Builder — стандартный способ создавать сложные объекты с опциональными параметрами.
Примеры из реальных библиотек:
// pog — PostgreSQL клиент
pog.default_config()
|> pog.host("db.example.com")
|> pog.port(5432)
|> pog.database("myapp")
|> pog.connect
// mist — HTTP сервер
mist.new(handler)
|> mist.port(8080)
|> mist.start
// telega — Telegram Bot
telega.new_for_polling(token: token)
|> telega.with_router(my_router)
|> telega.init_for_polling_nil_session()
Два вида builder
Конструирующий builder строит структуру данных пошагово. Каждый add_* / with_* возвращает новый builder с добавленным элементом, а финальный вызов build создаёт готовый объект:
let src =
new_dir("src")
|> add_file("main.gleam", 2048)
|> add_file("utils.gleam", 512)
|> build
// → Directory("src", [File("main.gleam", 2048), File("utils.gleam", 512)])
let project =
new_dir("project")
|> add_file("README.md", 1024)
|> add_subdir(src)
|> build
Сравните с прямым конструированием — при большом числе файлов это нечитаемо:
Directory("project", [
File("README.md", 1024),
Directory("src", [
File("main.gleam", 2048),
File("utils.gleam", 512),
]),
])
Конфигурирующий builder собирает набор параметров / фильтров без немедленного выполнения. Вместо данных он аккумулирует настройки, а финальный вызов (run, connect, start) применяет их:
// FsQuery — конфигурирующий builder для поиска файлов
new_query()
|> with_extension(".gleam")
|> with_min_size(1000)
|> run_query(root_dir)
// → ["main.gleam", "main_test.gleam"]
Ключевые свойства паттерна
- Начинается с
new_*()илиdefault_*()— создаёт builder с дефолтными значениями - Каждый
with_*/add_*возвращает новый builder (иммутабельно, через..) - Заканчивается
build/run/connect— создаёт финальный результат - Дружит с pipe
|>— код читается как пошаговый список действий
Тип DirBuilder и функции new_dir, add_file, add_subdir, build уже реализованы в src/chapter04.gleam как демонстрационный пример. В упражнении 7 вы реализуете конфигурирующий builder FsQuery.
Проект: модель файловой системы
Соберём изученные концепции, построив модель файловой системы. Определим рекурсивный тип — узел, который может быть файлом или директорией:
pub type FSNode {
File(name: String, size: Int)
Directory(name: String, children: List(FSNode))
}
Это рекурсивный тип: Directory содержит List(FSNode), то есть может хранить другие файлы и директории.
Создадим тестовое дерево:
let fs = Directory("project", [
File("README.md", 1024),
Directory("src", [
File("main.gleam", 2048),
File("utils.gleam", 512),
]),
Directory("test", [
File("main_test.gleam", 1536),
]),
])
Это соответствует структуре:
project/
├── README.md (1024 bytes)
├── src/
│ ├── main.gleam (2048 bytes)
│ └── utils.gleam (512 bytes)
└── test/
└── main_test.gleam (1536 bytes)
Тип FSNode определён в exercises/chapter04/src/chapter04.gleam — вы импортируете его в решениях. Все упражнения этой главы работают с этим типом, постепенно наращивая сложность: от простого обхода дерева до группировки данных с помощью Dict.
Упражнения
Решения пишите в файле exercises/chapter04/test/my_solutions.gleam. Тип FSNode уже импортирован. Запускайте тесты:
cd exercises/chapter04
gleam test
Команды для перехода в директорию упражнения и запуска тестов, проверяющих ваши решения.
1. total_size (Лёгкое)
Вычислите общий размер узла файловой системы. Для файла — его размер, для директории — сумма размеров всех вложенных узлов (рекурсивно).
pub fn total_size(node: FSNode) -> Int
Примеры:
total_size(File("a.txt", 100)) == 100
total_size(sample_fs) == 5120 // 1024 + 2048 + 512 + 1536
total_size(Directory("empty", [])) == 0
Подсказка: используйте case для разделения на File и Directory. Для директории пройдите children с помощью list.fold, рекурсивно вызывая total_size.
2. all_files (Лёгкое)
Соберите имена всех файлов в дереве в плоский список. Порядок — обход в глубину (depth-first).
pub fn all_files(node: FSNode) -> List(String)
Примеры:
all_files(sample_fs)
== ["README.md", "main.gleam", "utils.gleam", "main_test.gleam"]
all_files(File("only.txt", 10))
== ["only.txt"]
Подсказка: для файла верните список из одного имени. Для директории используйте list.flat_map — она применит all_files к каждому дочернему узлу и объединит результаты.
3. find_by_extension (Среднее)
Найдите все файлы с заданным расширением.
pub fn find_by_extension(node: FSNode, ext: String) -> List(String)
Примеры:
find_by_extension(sample_fs, ".gleam")
== ["main.gleam", "utils.gleam", "main_test.gleam"]
find_by_extension(sample_fs, ".md")
== ["README.md"]
find_by_extension(sample_fs, ".rs")
== []
Подсказка: похоже на all_files, но для файла проверяйте string.ends_with(name, ext). Если не совпадает — возвращайте пустой список.
4. largest_file (Среднее)
Найдите самый большой файл в дереве. Верните пару #(имя, размер) или Error(Nil), если файлов нет.
pub fn largest_file(node: FSNode) -> Result(#(String, Int), Nil)
Примеры:
largest_file(sample_fs) == Ok(#("main.gleam", 2048))
largest_file(Directory("empty", [])) == Error(Nil)
Подсказка: сначала соберите все файлы как пары #(имя, размер) (напишите вспомогательную функцию, аналогичную all_files). Затем используйте list.reduce для поиска максимума — она вернёт Error(Nil) для пустого списка.
5. count_by_extension (Среднее)
Подсчитайте количество файлов каждого типа (по расширению). Результат — список пар #(расширение, количество), отсортированный по расширению. Считайте, что все файлы имеют расширение формата .xxx.
pub fn count_by_extension(node: FSNode) -> List(#(String, Int))
Примеры:
count_by_extension(sample_fs)
== [#(".gleam", 3), #(".md", 1)]
Подсказка: используйте all_files для сбора имён, извлеките расширение через string.split(name, "."), сгруппируйте с list.group, посчитайте длины через dict.map_values, преобразуйте в отсортированный список.
6. group_by_directory (Сложное)
Сгруппируйте файлы по директории, в которой они непосредственно находятся. Результат — список пар #(имя_директории, список_файлов), отсортированный по имени директории. Директории без прямых файлов-потомков не включаются.
pub fn group_by_directory(node: FSNode) -> List(#(String, List(String)))
Примеры:
group_by_directory(sample_fs)
== [
#("project", ["README.md"]),
#("src", ["main.gleam", "utils.gleam"]),
#("test", ["main_test.gleam"]),
]
Подсказка: напишите вспомогательную рекурсивную функцию: для директории отфильтруйте прямых детей-файлов через list.filter_map, рекурсивно обработайте поддиректории, объедините результаты. В конце отсортируйте по имени.
7. FsQuery builder (Среднее)
Реализуйте конфигурирующий builder для поиска файлов. Тип FsQuery уже определён в src/chapter04.gleam — вам нужно реализовать пять функций:
pub fn new_query() -> FsQuery
pub fn with_extension(q: FsQuery, ext: String) -> FsQuery
pub fn with_min_size(q: FsQuery, size: Int) -> FsQuery
pub fn with_max_size(q: FsQuery, size: Int) -> FsQuery
pub fn run_query(q: FsQuery, node: FSNode) -> List(String)
Примеры:
new_query()
|> run_query(sample_fs)
== ["README.md", "main.gleam", "utils.gleam", "main_test.gleam"]
new_query()
|> with_extension(".gleam")
|> run_query(sample_fs)
== ["main.gleam", "utils.gleam", "main_test.gleam"]
new_query()
|> with_min_size(1000)
|> with_max_size(2000)
|> run_query(sample_fs)
== ["README.md", "main_test.gleam"] // 1024 и 1536 — в диапазоне
Подсказка: new_query создаёт FsQuery со всеми полями None. Каждый with_* возвращает FsQuery(..q, field: Some(value)). В run_query соберите пары #(name, size) (как в largest_file), затем пройдите их через list.filter_map, проверяя каждый фильтр через case.
Заключение
В этой главе мы изучили:
- Пользовательские типы — основу моделирования данных в Gleam
- Generics для создания обобщённых типов и функций
- Списки и богатый модуль
gleam/list - Dict, Set и Queue для различных задач
- Iterator для ленивых вычислений
- Builder-паттерн — стандартный идиом для объектов с опциональными параметрами
Эти инструменты покрывают подавляющее большинство задач по работе с данными. В следующей главе мы глубже разберём рекурсию, свёртки и обработку ошибок через Result и Option.