Функции и пайплайны
Эта глава посвящена функциям как значениям первого класса, pipe-оператору и use-выражениям.
- Цели главы
- Функции как значения первого класса
- Нет каррирования
- Анонимные функции
- Именованные аргументы
- Pipe-оператор
- Отладка пайплайнов с
echo - use-выражения
- Case-выражения и pattern matching
- Модуль gleam/function
- Модуль gleam/bool
- Модуль gleam/order
- Проект: адресная книга
- Упражнения
- Заключение
Цели главы
В этой главе мы:
- Научимся работать с функциями как со значениями
- Поймём, почему в Gleam нет каррирования
- Освоим pipe-оператор
|>— основной инструмент композиции - Разберём use-выражения — синтаксический сахар для callback-ов
- Познакомимся с pattern matching и case-выражениями
Функции как значения первого класса
В Gleam функции — такие же значения, как числа или строки. Их можно сохранять в переменные, передавать в другие функции и возвращать из функций.
import gleam/io
pub fn main() {
// Сохраняем функцию в переменную
let add_one = fn(x) { x + 1 }
// Вызываем через переменную
io.debug(add_one(5)) // 6
// Передаём функцию как аргумент
io.debug(apply_twice(add_one, 3)) // 5
}
pub fn apply_twice(f: fn(a) -> a, x: a) -> a {
f(f(x))
}
Функция apply_twice принимает функцию f и значение x, а затем применяет f к x дважды. Тип fn(a) -> a означает: «функция, которая принимает значение типа a и возвращает значение того же типа». Здесь a — параметр типа (generic), который будет подставлен конкретным типом при вызове.
Нет каррирования
В Haskell, OCaml и PureScript каждая функция принимает ровно один аргумент, а многоаргументные функции — это цепочки функций, возвращающих функции:
-- Haskell: add :: Int -> Int -> Int
-- На самом деле: add :: Int -> (Int -> Int)
add x y = x + y
add3 = add 3 -- частичное применение
В Gleam каррирования нет. Каждая функция принимает все аргументы сразу:
pub fn add(x: Int, y: Int) -> Int {
x + y
}
// ✗ Ошибка! Нельзя вызвать add(3) — нужны оба аргумента
// let add3 = add(3)
// ✓ Для частичного применения используйте анонимную функцию
let add3 = fn(y) { add(3, y) }
Это осознанное решение: код без каррирования проще читать, а сообщения об ошибках — понятнее.
Захват функций (function capture)
Вместо анонимных функций-обёрток Gleam предлагает сокращённый синтаксис — function capture. Символ _ обозначает место для аргумента:
// Полная запись
let add3 = fn(y) { add(3, y) }
// Захват функции — то же самое, но короче
let add3 = add(3, _)
add(3, _) создаёт новую функцию, которая принимает один аргумент и подставляет его вместо _. Это единственный способ частичного применения в Gleam — явный и наглядный.
Анонимные функции
Анонимные (лямбда) функции создаются ключевым словом fn:
let double = fn(x) { x * 2 }
let greet = fn(name) { "Привет, " <> name <> "!" }
let add = fn(a, b) { a + b }
Анонимные функции часто используются как аргументы функций высшего порядка:
import gleam/list
pub fn main() {
let numbers = [1, 2, 3, 4, 5]
// Удвоить каждый элемент
list.map(numbers, fn(x) { x * 2 })
// [2, 4, 6, 8, 10]
// Оставить только чётные
list.filter(numbers, fn(x) { x % 2 == 0 })
// [2, 4]
}
Пример демонстрирует передачу анонимных функций в list.map и list.filter для трансформации и фильтрации списков.
Замыкания
Анонимные функции захватывают переменные из окружающего контекста:
pub fn make_adder(n: Int) -> fn(Int) -> Int {
fn(x) { x + n } // n захвачена из внешней области
}
pub fn main() {
let add5 = make_adder(5)
add5(3) // 8
add5(10) // 15
}
Захваченные значения неизменяемы — замыкание видит тот n, который существовал на момент создания функции.
Именованные аргументы
Gleam поддерживает именованные (labelled) аргументы. Они позволяют указывать имя параметра при вызове для ясности:
pub fn greet(greeting greeting: String, name name: String) -> String {
greeting <> ", " <> name <> "!"
}
В сигнатуре greeting greeting: String первое слово — внешняя метка (для вызова), второе — внутреннее имя (для тела функции). Если они совпадают, можно указать имя один раз:
pub fn greet(greeting greeting: String, name name: String) -> String
// то же самое, что:
pub fn greet(greeting: String, name: String) -> String
При вызове именованные аргументы можно передавать в любом порядке:
greet(greeting: "Привет", name: "Алиса")
// "Привет, Алиса!"
greet(name: "Боб", greeting: "Здравствуй")
// "Здравствуй, Боб!"
Именованные аргументы особенно полезны для функций с несколькими параметрами одного типа, где порядок неочевиден:
// Без меток — что есть что?
send("alice@example.com", "bob@example.com", "Привет!")
// С метками — ясно
send(from: "alice@example.com", to: "bob@example.com", body: "Привет!")
Именованные аргументы устраняют неоднозначность при нескольких параметрах одного типа: по меткам сразу видно, что означает каждый аргумент.
Сокращённый синтаксис меток
Если локальная переменная называется так же, как именованный аргумент, имя можно не повторять:
let name = "Алиса"
let greeting = "Привет"
// Полная запись
greet(greeting: greeting, name: name)
// Сокращённая — то же самое
greet(greeting:, name:)
Запись name: без значения означает: «подставь переменную name». Это работает и при создании записей (custom types с labelled fields).
Pipe-оператор
Pipe-оператор |> — главный инструмент композиции в Gleam. Он передаёт результат левого выражения как первый аргумент правого:
// Без pipe
string.uppercase(string.trim(" hello "))
// С pipe — читается слева направо, сверху вниз
" hello "
|> string.trim
|> string.uppercase
Оба варианта делают одно и то же, но второй читается как последовательность шагов: «взять строку, обрезать пробелы, перевести в верхний регистр».
Pipe с дополнительными аргументами
Если функция принимает больше одного аргумента, pipe подставляет значение как первый:
import gleam/string
"hello"
|> string.append(", world") // string.append("hello", ", world")
|> string.uppercase // string.uppercase("hello, world")
// "HELLO, WORLD"
Pipe-оператор автоматически подставляет предыдущее значение первым аргументом, поэтому string.append получает "hello" как базу и добавляет ", world".
Pipe с function capture
По умолчанию |> подставляет значение как первый аргумент. Если нужно подставить в другую позицию, используйте function capture с _:
import gleam/string
// _ указывает, куда подставить значение из pipe
"world"
|> string.append("hello, ", _)
|> string.uppercase
// "HELLO, WORLD"
Здесь string.append("hello, ", _) создаёт функцию, которая подставляет входящее значение вторым аргументом. Без _ получилось бы string.append("world", "hello, ") — не то, что нужно.
Ещё пример — построение строки справа налево:
"1"
|> string.append("2") // string.append("1", "2") → "12"
|> string.append("3", _) // string.append("3", "12") → "312"
Символ _ в function capture позволяет передать значение из pipe в любую позицию аргумента, а не только в первую.
Практический пример
Pipe-оператор позволяет строить читаемые конвейеры обработки данных:
import gleam/int
import gleam/list
import gleam/string
pub fn format_scores(scores: List(Int)) -> String {
scores
|> list.sort(int.compare)
|> list.reverse
|> list.map(int.to_string)
|> string.join(", ")
}
// format_scores([42, 17, 88, 5]) == "88, 42, 17, 5"
Функция format_scores сортирует результаты по убыванию и объединяет их в строку через запятую — весь конвейер записан без промежуточных переменных.
Отладка пайплайнов с echo
Проблема отладки в пайплайнах
При работе с длинными цепочками pipe-операторов бывает сложно понять, какое значение имеет выражение на промежуточном этапе. Классический подход — разбить pipeline на шаги с промежуточными переменными или вставить вызов io.debug:
import gleam/int
import gleam/list
import gleam/string
pub fn format_scores(scores: List(Int)) -> String {
scores
|> list.sort(int.compare)
|> io.debug // выводит отсортированный список
|> list.reverse
|> io.debug // выводит перевёрнутый список
|> list.map(int.to_string)
|> string.join(", ")
}
Проблема этого подхода:
io.debugвыводит только значение, без контекста (где именно это напечатано?)- Приходится вручную добавлять и удалять вызовы
io.debug - Нет информации о файле и номере строки
Решение: ключевое слово echo
Ключевое слово echo было добавлено в Gleam v1.9.0 специально для удобной отладки пайплайнов. Синтаксис:
value |> echo
// или с меткой
value |> echo("после сортировки")
echo печатает значение в консоль и возвращает его без изменений, позволяя продолжить цепочку pipe. Но в отличие от io.debug, echo автоматически показывает:
- Имя файла
- Номер строки
- Опциональную метку
- Само значение
Пример с echo:
import gleam/int
import gleam/list
import gleam/string
pub fn format_scores(scores: List(Int)) -> String {
scores
|> list.sort(int.compare)
|> echo("после сортировки")
|> list.reverse
|> echo("после разворота")
|> list.map(int.to_string)
|> string.join(", ")
}
Вывод в консоль:
src/chapter03.gleam:8: после сортировки
[5, 17, 42, 88]
src/chapter03.gleam:10: после разворота
[88, 42, 17, 5]
Сразу видно, где в коде произошёл вывод и какое значение было на этом этапе.
echo vs io.debug()
| Функция | Показывает файл:строку | Принимает метку | Возвращает значение |
|---|---|---|---|
echo | ✓ | ✓ (опционально) | ✓ |
io.debug | ✗ | ✗ | ✓ |
io.debug полезен для быстрого вывода в консоль, но для отладки сложных пайплайнов echo удобнее — не нужно гадать, в каком месте кода произошёл вывод.
Ещё одно отличие: echo можно использовать без импорта. Это встроенное ключевое слово языка, а не функция из модуля.
Практические примеры
Отладка трансформации данных
Представим, что нужно обработать список чисел: оставить только положительные, удвоить их и найти сумму. На каком этапе список стал пустым?
import gleam/int
import gleam/list
pub fn process_numbers(numbers: List(Int)) -> Int {
numbers
|> echo("исходный список")
|> list.filter(fn(n) { n > 0 })
|> echo("после фильтрации")
|> list.map(fn(n) { n * 2 })
|> echo("после удвоения")
|> int.sum
}
pub fn main() {
process_numbers([1, -2, 3, -4, 5])
|> io.debug
}
Вывод:
src/chapter03.gleam:6: исходный список
[1, -2, 3, -4, 5]
src/chapter03.gleam:8: после фильтрации
[1, 3, 5]
src/chapter03.gleam:10: после удвоения
[2, 6, 10]
18
Каждый шаг видимым — легко понять, как преобразуются данные.
Отладка строковых операций
Сложные преобразования строк тоже удобно отлаживать с echo:
import gleam/string
pub fn normalize_name(name: String) -> String {
name
|> echo("входная строка")
|> string.trim
|> echo("после trim")
|> string.lowercase
|> echo("после lowercase")
|> string.replace("_", " ")
|> echo("после замены _")
}
// normalize_name(" John_DOE ")
Вывод:
src/chapter03.gleam:5: входная строка
" John_DOE "
src/chapter03.gleam:7: после trim
"John_DOE"
src/chapter03.gleam:9: после lowercase
"john_doe"
src/chapter03.gleam:11: после замены _
"john doe"
Отладка вложенных трансформаций
echo работает на любом уровне вложенности:
import gleam/int
import gleam/list
import gleam/string
pub fn top_three_doubled(numbers: List(Int)) -> String {
numbers
|> echo("начальный список")
|> list.sort(int.compare)
|> list.reverse
|> echo("отсортировано по убыванию")
|> list.take(3)
|> echo("взято первые 3")
|> list.map(fn(n) {
let doubled = n * 2
echo(doubled, "удвоенное значение")
doubled
})
|> echo("после удвоения")
|> list.map(int.to_string)
|> string.join(", ")
}
Вывод показывает все промежуточные значения — даже внутри list.map.
Когда использовать echo
✓ Используйте echo, когда:
- Отлаживаете длинный pipeline
- Хотите проверить промежуточные значения
- Нужно понять, на каком шаге данные меняются неожиданным образом
- Изучаете новую функцию или библиотеку
✗ Не используйте echo для:
- Production-логирования (используйте библиотеки логирования)
- Вывода результатов пользователю (используйте
io.println) - Постоянной отладки (используйте debugger или тесты)
echo — временный инструмент для разработки. Перед коммитом кода удалите все вызовы echo (либо замените их на полноценное логирование, если нужно).
use-выражения
use-выражение — синтаксический сахар для callback-ов. Это не монадический do из Haskell — это простая перезапись вложенных callback-функций в плоский код.
Проблема: вложенные callback-и
Представьте цепочку операций, каждая из которых может завершиться ошибкой:
import gleam/result
pub fn process(input: String) -> Result(Int, String) {
result.try(parse_input(input), fn(value) {
result.try(validate(value), fn(valid) {
Ok(valid * 2)
})
})
}
С каждым шагом вложенность растёт. Код сложно читать.
Решение: use
use разворачивает callback в плоский стиль:
import gleam/result
pub fn process(input: String) -> Result(Int, String) {
use value <- result.try(parse_input(input))
use valid <- result.try(validate(value))
Ok(valid * 2)
}
Запись use x <- f(arg) означает: «вызови f(arg, fn(x) { ... }), где ... — весь оставшийся код блока». Другими словами, use захватывает остаток блока как callback.
Как use работает внутри
Компилятор преобразует:
use x <- f(a)
use y <- g(x)
h(y)
в:
f(a, fn(x) {
g(x, fn(y) {
h(y)
})
})
Никакой магии — только устранение вложенности.
use с несколькими значениями
use может связывать несколько переменных:
use first, rest <- list.pop(items)
Это эквивалентно list.pop(items, fn(first, rest) { ... }).
Case-выражения и pattern matching
Case-выражение — основной способ ветвления в Gleam:
pub fn describe(n: Int) -> String {
case n {
0 -> "ноль"
1 -> "один"
_ -> "другое число"
}
}
_ — подстановочный паттерн, совпадающий с любым значением.
Pattern matching на строках
pub fn greet(name: String) -> String {
case name {
"Алиса" -> "Привет, Алиса! Как дела?"
"Боб" -> "Здорово, Боб!"
name -> "Привет, " <> name <> "!"
}
}
В последней ветке name — новая переменная, привязанная к значению.
Сопоставление нескольких значений
Case может сопоставлять несколько значений одновременно:
import gleam/int
pub fn fizzbuzz(n: Int) -> String {
case n % 3, n % 5 {
0, 0 -> "FizzBuzz"
0, _ -> "Fizz"
_, 0 -> "Buzz"
_, _ -> int.to_string(n)
}
}
Пример показывает сопоставление с образцом по нескольким значениям одновременно с помощью кортежа в case.
Guards
Guards — дополнительные условия в ветках case:
pub fn classify_age(age: Int) -> String {
case age {
a if a < 0 -> "некорректный возраст"
a if a < 13 -> "ребёнок"
a if a < 18 -> "подросток"
a if a < 65 -> "взрослый"
_ -> "пенсионер"
}
}
Guards используют ключевое слово if после паттерна. В них допустимы сравнения и логические операторы.
Альтернативные паттерны
Несколько паттернов можно объединить оператором | (ИЛИ):
pub fn is_weekend(day: String) -> Bool {
case day {
"Saturday" | "Sunday" -> True
_ -> False
}
}
Оператор | в паттернах позволяет объединять несколько вариантов в одну ветку, избегая дублирования логики.
Модуль gleam/function
Модуль gleam/function содержит утилиты для работы с функциями:
import gleam/function
// identity — возвращает аргумент без изменений
function.identity(42) // 42
// compose — композиция двух функций
let double_then_add1 = function.compose(
fn(x) { x * 2 },
fn(x) { x + 1 },
)
double_then_add1(5) // 11
// flip — меняет порядок аргументов
let flipped_append = function.flip(string.append)
flipped_append("мир", "привет, ") // "привет, мир"
// tap — выполняет побочный эффект и возвращает исходное значение
42
|> function.tap(io.debug) // выводит 42, возвращает 42
|> int.to_string
Эти утилиты особенно полезны при построении конвейеров из pipe-операторов, когда нужно адаптировать порядок аргументов или вставить отладочный вывод без разрыва цепочки.
Модуль gleam/bool
Утилиты для работы с логическими значениями:
import gleam/bool
bool.to_int(True) // 1
bool.to_int(False) // 0
bool.to_string(True) // "True"
bool.negate(True) // False
// guard — раннее прерывание при выполнении условия
pub fn divide(a: Int, b: Int) -> Result(Int, String) {
use <- bool.guard(b == 0, Error("деление на ноль"))
Ok(a / b)
}
bool.guard(condition, return_value) — если condition равно True, немедленно возвращает return_value. Иначе выполняет оставшийся код. Это идиоматический способ «раннего возврата» в Gleam.
Модуль gleam/order
Тип Order используется для сравнения значений:
import gleam/order
// Тип Order имеет три значения: Lt, Eq, Gt
order.compare(1, 2) // Lt
order.compare(2, 2) // Eq
order.compare(3, 2) // Gt
// Обращение порядка
order.negate(order.Lt) // Gt
// Полезно для сортировки
order.reverse // функция для обратной сортировки
Тип Order и его функции часто используются совместно с сортировкой списков — например, передаётся в list.sort для управления порядком элементов.
Проект: адресная книга
Соберём изученные концепции в небольшом проекте. Представим адресную книгу как список записей:
import gleam/io
import gleam/list
import gleam/string
pub type Entry {
Entry(name: String, phone: String, city: String)
}
pub fn format_entry(entry: Entry) -> String {
entry.name <> " (" <> entry.city <> "): " <> entry.phone
}
pub fn find_by_city(
entries: List(Entry),
city: String,
) -> List(Entry) {
entries
|> list.filter(fn(e) { e.city == city })
}
pub fn format_book(entries: List(Entry)) -> String {
entries
|> list.map(format_entry)
|> string.join("\n")
}
pub fn main() {
let book = [
Entry(name: "Алиса", phone: "+7-900-111-22-33", city: "Москва"),
Entry(name: "Боб", phone: "+7-900-444-55-66", city: "Петербург"),
Entry(name: "Чарли", phone: "+7-900-777-88-99", city: "Москва"),
]
book
|> find_by_city("Москва")
|> format_book
|> io.println
// Алиса (Москва): +7-900-111-22-33
// Чарли (Москва): +7-900-777-88-99
}
Обратите внимание, как pipe-оператор превращает цепочку вызовов в читаемый конвейер. Мы ещё не знакомы с пользовательскими типами (они будут в следующей главе), но уже можем видеть, как Entry описывает структуру данных с именованными полями.
Упражнения
Решения пишите в файле exercises/chapter03/test/my_solutions.gleam. Запускайте тесты:
cd exercises/chapter03
gleam test
Запускайте тесты после каждого упражнения, чтобы сразу видеть результат.
1. apply_twice (Лёгкое)
Напишите функцию apply_twice, которая применяет функцию f к значению x дважды.
pub fn apply_twice(f: fn(a) -> a, x: a) -> a
Примеры:
apply_twice(fn(x) { x * 2 }, 3) == 12
apply_twice(fn(x) { x + 1 }, 0) == 2
Примеры показывают ожидаемое поведение функции apply_twice при применении функции к значению дважды.
2. add_exclamation (Лёгкое)
Напишите функцию, которая добавляет восклицательный знак к строке.
pub fn add_exclamation(s: String) -> String
Примеры:
add_exclamation("hello") == "hello!"
add_exclamation("") == "!"
Примеры показывают ожидаемое поведение функции add_exclamation для строк разной длины.
3. shout (Среднее)
Напишите функцию, которая переводит строку в верхний регистр и добавляет "!". Используйте pipe-оператор.
pub fn shout(s: String) -> String
Примеры:
shout("hello") == "HELLO!"
shout("gleam") == "GLEAM!"
Подсказка: используйте string.uppercase и функцию add_exclamation из предыдущего упражнения (или оператор <>).
4. safe_divide (Среднее)
Напишите функцию безопасного целочисленного деления. При делении на ноль возвращайте ошибку.
pub fn safe_divide(a: Int, b: Int) -> Result(Int, String)
Примеры:
safe_divide(10, 3) == Ok(3)
safe_divide(10, 0) == Error("деление на ноль")
Подсказка: используйте case-выражение.
5. FizzBuzz (Среднее)
Напишите функцию FizzBuzz: если число делится на 15 → "FizzBuzz", на 3 → "Fizz", на 5 → "Buzz", иначе → строковое представление числа.
pub fn fizzbuzz(n: Int) -> String
Примеры:
fizzbuzz(15) == "FizzBuzz"
fizzbuzz(9) == "Fizz"
fizzbuzz(10) == "Buzz"
fizzbuzz(7) == "7"
Подсказка: используйте case с сопоставлением нескольких значений — case n % 3, n % 5 { ... }.
Заключение
В этой главе мы изучили:
- Функции как значения первого класса и анонимные функции
- Отсутствие каррирования в Gleam и способы частичного применения
- Pipe-оператор
|>для построения конвейеров - Ключевое слово
echoдля отладки пайплайнов - use-выражения для устранения вложенных callback-ов
- Case-выражения, pattern matching и guards
- Модули
gleam/function,gleam/boolиgleam/order
В следующей главе мы познакомимся с пользовательскими типами, generics и основными коллекциями Gleam.