Строки, битовые массивы и стандартная библиотека
«Строки — не байты. Байты — не строки.» — Эрланговская мудрость
- Цели главы
- Строки в Gleam
- gleam/string — полный обзор
- String builder (gleam/string_tree)
- Битовые массивы
- Pattern matching на bit arrays
- Bytes builder (gleam/bytes_tree)
- Регулярные выражения (gleam/regexp)
- URI (gleam/uri)
- gleam/pair
- Проект: HTML builder
- Упражнения
- Заключение
Цели главы
В этой главе мы:
- Детально изучим строки в Gleam: UTF-8, графемы, кодпоинты
- Разберём модуль
gleam/string— полный обзор функций - Познакомимся с
string_treeдля эффективной конкатенации - Изучим битовые массивы (bit arrays) и паттерн-матчинг на них
- Освоим
gleam/regexpдля работы с регулярными выражениями - Разберём
gleam/uriдля работы с URL - Познакомимся с
gleam/pair— утилитами для кортежей
Строки в Gleam
Строки в Gleam — это иммутабельные последовательности символов в кодировке UTF-8. На BEAM-таргете они реализованы как Erlang-бинарники (binary), на JavaScript — как JS-строки.
UTF-8 и графемы
UTF-8 — кодировка переменной длины: символ может занимать от 1 до 4 байтов. Это создаёт важное различие:
import gleam/string
// Длина в графемах (пользовательских символах)
string.length("hello") // 5
string.length("привет") // 6
// Размер в байтах
string.byte_size("hello") // 5 — ASCII: 1 байт на символ
string.byte_size("привет") // 12 — кириллица: 2 байта на символ
Графема (grapheme) — то, что пользователь воспринимает как один символ. Функция string.length считает графемы, а не байты. Это правильное поведение для большинства задач.
Кодпоинты
Кодпоинт (codepoint) — числовой код символа в Unicode:
// Строка → список кодпоинтов
let cps = string.to_utf_codepoints("Abc")
// [UtfCodepoint(65), UtfCodepoint(98), UtfCodepoint(99)]
// Кодпоинт → число
let assert [cp, ..] = cps
string.utf_codepoint_to_int(cp) // 65 — код буквы 'A'
// Число → кодпоинт
let assert Ok(cp) = string.utf_codepoint(97) // 97 = 'a'
string.from_utf_codepoints([cp]) // "a"
Кодпоинты полезны при работе с символами на уровне Unicode — например, для шифров или транслитерации.
gleam/string — полный обзор
Модуль gleam/string содержит около 40 функций. Разберём их по группам.
Базовые
string.length("hello") // 5
string.is_empty("") // True
string.is_empty("x") // False
string.reverse("hello") // "olleh"
string.compare("a", "b") // order.Lt
string.byte_size("привет") // 12
Базовые функции охватывают наиболее частые операции: проверку длины и пустоты строки, разворот символов и лексикографическое сравнение через order.Lt/order.Eq/order.Gt.
Регистр
string.lowercase("HELLO") // "hello"
string.uppercase("hello") // "HELLO"
string.capitalise("hello") // "Hello"
capitalise переводит первую букву в верхний регистр, остальные — в нижний.
Поиск
string.contains("hello world", "world") // True
string.starts_with("hello", "hel") // True
string.ends_with("file.gleam", ".gleam") // True
Функции поиска возвращают Bool и позволяют быстро проверить вхождение подстроки без создания лишних промежуточных значений.
Разбиение
// split — по разделителю
string.split("a,b,c", ",") // ["a", "b", "c"]
// split_once — только первое вхождение
string.split_once("a:b:c", ":") // Ok(#("a", "b:c"))
// to_graphemes — в список графем
string.to_graphemes("hello") // ["h", "e", "l", "l", "o"]
// pop_grapheme — отделить первый символ
string.pop_grapheme("hello") // Ok(#("h", "ello"))
string.pop_grapheme("") // Error(Nil)
Функции разбиения позволяют разложить строку на части: split и split_once делят по разделителю, а to_graphemes и pop_grapheme работают с отдельными символами.
Сборка
// append — конкатенация двух строк
string.append("hello", " world") // "hello world"
// concat — конкатенация списка строк
string.concat(["hello", " ", "world"]) // "hello world"
// join — конкатенация с разделителем
string.join(["a", "b", "c"], ", ") // "a, b, c"
// repeat — повторение строки
string.repeat("ha", 3) // "hahaha"
Оператор <> тоже конкатенирует строки: "hello" <> " world".
Обрезка
// trim — убрать пробелы с обоих сторон
string.trim(" hello ") // "hello"
string.trim_start(" hello ") // "hello "
string.trim_end(" hello ") // " hello"
// pad — дополнить до заданной длины
string.pad_start("42", 5, "0") // "00042"
string.pad_end("hi", 10, ".") // "hi........"
Функции обрезки убирают лишние пробелы с одного или обоих концов строки, а pad_start/pad_end дополняют строку до нужной длины заданным символом.
Слайсы
// slice — подстрока по позиции и длине
string.slice("hello world", 6, 5) // "world"
// crop — подстрока от первого вхождения до конца
string.crop("hello world", "world") // "world"
// drop_start / drop_end — отбросить символы
string.drop_start("hello", 2) // "llo"
string.drop_end("hello", 2) // "hel"
// first / last — первый/последний символ
string.first("hello") // Ok("h")
string.last("hello") // Ok("o")
string.first("") // Error(Nil)
Функции срезов позволяют извлекать подстроки по позиции и длине (slice), от первого вхождения до конца (crop), а также получать или отбрасывать крайние символы.
Замена
string.replace("hello world", "world", "gleam")
// "hello gleam"
// Заменяет ВСЕ вхождения
string.replace("abcabc", "a", "x")
// "xbcxbc"
string.replace заменяет все вхождения подстроки в строке — эквивалент глобального поиска и замены без регулярных выражений.
Утилиты
// inspect — строковое представление любого значения
string.inspect(42) // "42"
string.inspect([1, 2, 3]) // "[1, 2, 3]"
string.inspect(Ok("hi")) // "Ok(\"hi\")"
// to_option — пустая строка → None
string.to_option("") // None
string.to_option("hello") // Some("hello")
string.inspect полезна при отладке — она превращает любое значение Gleam в читаемую строку. string.to_option удобна для преобразования пустых строк в None при работе с опциональными значениями.
String builder (gleam/string_tree)
Конкатенация строк через <> создаёт копию на каждом шаге — это O(n) для каждой операции. Для сборки строк из множества частей используйте string_tree:
import gleam/string_tree
let tree =
string_tree.new()
|> string_tree.append("Hello")
|> string_tree.append(", ")
|> string_tree.append("world!")
|> string_tree.to_string
// "Hello, world!"
На BEAM string_tree реализован как IO-лист (iolist) — дерево строк, которое «разворачивается» в плоскую строку только при необходимости. Это O(1) для каждого append.
Когда использовать string_tree?
- Сборка HTML/JSON/шаблонов из множества частей
- Конкатенация в цикле/свёртке
- Генерация больших текстов
Для простой конкатенации 2-3 строк <> достаточно.
Основные функции
// Создание
string_tree.new() // пустой
string_tree.from_string("hello") // из строки
string_tree.from_strings(["a", "b"]) // из списка строк
// Добавление
string_tree.append(tree, " suffix") // добавить строку
string_tree.prepend(tree, "prefix ") // добавить в начало
string_tree.append_tree(tree1, tree2) // объединить деревья
// Преобразование
string_tree.to_string(tree) // → String
string_tree.byte_size(tree) // размер в байтах
string_tree.is_empty(tree) // пусто ли
// Утилиты
string_tree.join([tree1, tree2], ", ") // объединить с разделителем
string_tree.reverse(tree) // развернуть
string_tree предоставляет те же операции, что и gleam/string, но без промежуточных копий: каждый append и prepend работает за O(1), а итоговая строка собирается один раз при вызове to_string.
Битовые массивы
Битовые массивы (bit arrays) — последовательности байтов. Они используются для работы с бинарными данными: файлами, сетевыми протоколами, кодеками.
Создание
// Литерал
let bytes = <<1, 2, 3>> // три байта
// Из строки
let text = <<"hello":utf8>> // строка как UTF-8 байты
// Числа с указанием размера
let big = <<1024:16>> // 16-битное число (2 байта)
let small = <<42:8>> // 8-битное число (1 байт)
// Конкатенация
let combined = <<bytes:bits, text:bits>>
Литерал <<...>> позволяет задавать байты напрямую, встраивать строки как UTF-8, указывать разрядность чисел и конкатенировать несколько битовых массивов в один.
gleam/bit_array — API
import gleam/bit_array
// Конвертация строка ↔ байты
bit_array.from_string("hello") // <<"hello":utf8>>
bit_array.to_string(<<"hello":utf8>>) // Ok("hello")
// Размер
bit_array.byte_size(<<1, 2, 3>>) // 3
// Срез
bit_array.slice(<<1, 2, 3, 4, 5>>, 1, 3) // Ok(<<2, 3, 4>>)
// Конкатенация
bit_array.concat([<<1, 2>>, <<3, 4>>]) // <<1, 2, 3, 4>>
// Проверка UTF-8
bit_array.is_utf8(<<"hello":utf8>>) // True
bit_array.is_utf8(<<255, 254>>) // False
// Base64
bit_array.base64_encode(<<"hello":utf8>>, True)
// "aGVsbG8="
bit_array.base64_decode("aGVsbG8=")
// Ok(<<"hello":utf8>>)
// Base16 (hex)
bit_array.base16_encode(<<255, 0, 127>>)
// "FF007F"
bit_array.base16_decode("FF007F")
// Ok(<<255, 0, 127>>)
// Inspect — для отладки
bit_array.inspect(<<72, 101>>) // "<<72, 101>>"
Модуль gleam/bit_array предоставляет инструменты для преобразования бинарных данных: конвертацию строк, срезы, конкатенацию, кодирование в base64 и base16, а также проверку валидности UTF-8.
Pattern matching на bit arrays
Одна из самых мощных возможностей Gleam (унаследованная от Erlang) — паттерн-матчинг на битовых массивах:
import gleam/int
pub fn classify_ip(data: BitArray) -> String {
case data {
// IPv4: 4 байта
<<a:8, b:8, c:8, d:8>> -> {
"IPv4: "
<> int.to_string(a) <> "."
<> int.to_string(b) <> "."
<> int.to_string(c) <> "."
<> int.to_string(d)
}
_ -> "unknown"
}
}
classify_ip(<<192, 168, 1, 1>>)
// "IPv4: 192.168.1.1"
Паттерн-матчинг на битовых массивах позволяет деструктурировать бинарные данные прямо в case-выражении, указывая точный размер каждого сегмента в битах — без ручного смещения и побитовых операций.
Размеры сегментов
case data {
// Фиксированный размер в битах
<<header:16, payload:bytes>> -> ...
// Магические байты
<<0x89, "PNG":utf8, rest:bytes>> -> "PNG file"
// Конкретные значения байтов
<<0xCA, 0xFE, rest:bytes>> -> ...
}
Доступные спецификаторы сегментов:
:8,:16,:32,:64— размер в битах:bytes— остаток как байты:bits— остаток как биты:utf8— UTF-8 строка:float— 64-битное число с плавающей точкой
Пример: простой парсер бинарного формата
import gleam/bit_array
import gleam/result
pub type Packet {
Packet(version: Int, payload: BitArray)
}
pub fn parse_packet(data: BitArray) -> Result(Packet, Nil) {
case data {
<<version:8, len:16, payload:bytes>> if bit_array.byte_size(payload) >= len ->
Ok(Packet(version:, payload: bit_array.slice(payload, 0, len) |> result.unwrap(<<>>)))
_ -> Error(Nil)
}
}
Формат использует структуру TLV (Type-Length-Value):
- Первый байт (
version:8) — версия протокола - Следующие 2 байта (
len:16) — длина полезной нагрузки (big-endian, до 65535) - Остаток (
payload:bytes) — данные произвольной длины
Гард if bit_array.byte_size(payload) >= len защищает от обрезанных пакетов: если данных меньше, чем заявлено в заголовке, возвращаем Error(Nil). Затем bit_array.slice извлекает ровно len байтов — всё, что после, игнорируется (возможно, это следующий пакет).
Bytes builder (gleam/bytes_tree)
Аналог string_tree, но для байтов:
import gleam/bytes_tree
let tree =
bytes_tree.new()
|> bytes_tree.append(<<1, 2, 3>>)
|> bytes_tree.append(<<4, 5>>)
|> bytes_tree.to_bit_array
// <<1, 2, 3, 4, 5>>
bytes_tree используется при построении бинарных протоколов и HTTP-ответов (Wisp использует его для тел ответов).
Основные функции
bytes_tree.new() // пустой
bytes_tree.from_bit_array(<<1, 2>>) // из BitArray
bytes_tree.from_string("hello") // из строки (UTF-8)
bytes_tree.from_string_tree(st) // из StringTree
bytes_tree.append(tree, <<3, 4>>) // добавить байты
bytes_tree.append_string(tree, "text") // добавить строку
bytes_tree.prepend(tree, <<0>>) // добавить в начало
bytes_tree.concat([tree1, tree2]) // объединить список
bytes_tree.to_bit_array(tree) // → BitArray
bytes_tree.byte_size(tree) // размер в байтах
bytes_tree предлагает тот же набор операций, что и string_tree, но работает с BitArray вместо строк — удобно при сборке бинарных пакетов или HTTP-тел из нескольких фрагментов.
Регулярные выражения (gleam/regexp)
Модуль gleam/regexp предоставляет поддержку регулярных выражений:
import gleam/regexp
// Компиляция и проверка
let assert Ok(re) = regexp.from_string("^[a-z]+$")
regexp.check(re, "hello") // True
regexp.check(re, "Hello") // False
regexp.check(re, "123") // False
Функция regexp.check проверяет, совпадает ли строка с регулярным выражением: сначала регулярное выражение компилируется один раз через from_string, а затем многократно применяется без повторной компиляции.
Компиляция
// from_string — простая компиляция
regexp.from_string("[0-9]+") // Ok(Regexp) или Error(CompileError)
// compile — с опциями
regexp.compile("[a-z]+", regexp.Options(
case_insensitive: True,
multi_line: False,
))
// Ok(Regexp) — регистронезависимый поиск
from_string компилирует регулярное выражение и возвращает Result — ошибка компиляции (неверный синтаксис) обрабатывается явно, а compile позволяет задать дополнительные флаги, такие как регистронезависимость.
Поиск совпадений
let assert Ok(re) = regexp.from_string("(\\w+)@(\\w+)")
// scan — найти все совпадения
regexp.scan(re, "alice@example bob@test")
// [
// Match(content: "alice@example", submatches: [Some("alice"), Some("example")]),
// Match(content: "bob@test", submatches: [Some("bob"), Some("test")]),
// ]
Тип Match содержит:
content— полное совпадениеsubmatches— список захваченных групп (в скобках), каждаяOption(String)
Разбиение и замена
let assert Ok(re) = regexp.from_string("[,;\\s]+")
// split — разбить строку по регулярному выражению
regexp.split(re, "a, b; c d")
// ["a", "b", "c", "d"]
// replace — заменить совпадения
let assert Ok(digits) = regexp.from_string("[0-9]")
regexp.replace(digits, "h3ll0 w0rld", "*")
// "h*ll* w*rld"
regexp.split разбивает строку по регулярному выражению (удобно для разделителей переменной длины), а regexp.replace заменяет все совпадения на указанную строку.
Практический пример: парсинг логов
pub type LogEntry {
LogEntry(level: String, message: String)
}
pub fn parse_log(line: String) -> Result(LogEntry, Nil) {
let assert Ok(re) = regexp.from_string("\\[(\\w+)\\] (.+)")
case regexp.scan(re, line) {
[regexp.Match(_, [Some(level), Some(message)])] ->
Ok(LogEntry(level:, message:))
_ -> Error(Nil)
}
}
parse_log("[ERROR] connection timeout")
// Ok(LogEntry(level: "ERROR", message: "connection timeout"))
Разберём по шагам:
- Регулярное выражение
\[(\w+)\] (.+)содержит две группы захвата (в скобках):(\w+)— одно или более «словесных» символов (уровень лога), и(.+)— всё после пробела (сообщение) regexp.scanвозвращает списокMatch— мы ожидаем ровно одно совпадение- Паттерн
[regexp.Match(_, [Some(level), Some(message)])]деструктурирует результат:_— полное совпадение (нам не нужно),Some(level)иSome(message)— захваченные группы - Если строка не соответствует формату — попадаем в
_ -> Error(Nil)
URI (gleam/uri)
Модуль gleam/uri для работы с URL и URI:
import gleam/uri
// Парсинг
let assert Ok(u) = uri.parse("https://example.com:8080/path?key=value#section")
u.scheme // Some("https")
u.host // Some("example.com")
u.port // Some(8080)
u.path // "/path"
u.query // Some("key=value")
u.fragment // Some("section")
// Сборка обратно в строку
uri.to_string(u)
// "https://example.com:8080/path?key=value#section"
uri.parse разбирает URL на составные части (схема, хост, порт, путь, параметры, фрагмент), возвращая Result, а uri.to_string собирает URI обратно в строку из этих полей.
Query-параметры
// Парсинг query string
uri.parse_query("name=Alice&age=30")
// Ok([#("name", "Alice"), #("age", "30")])
// Сборка query string
uri.query_to_string([#("search", "gleam lang"), #("page", "1")])
// "search=gleam+lang&page=1"
uri.parse_query разбирает строку параметров в список пар ключ-значение, а uri.query_to_string выполняет обратное преобразование с автоматическим percent-кодированием пробелов и спецсимволов.
Кодирование
// Percent-encoding
uri.percent_encode("hello world!") // "hello%20world%21"
uri.percent_decode("hello%20world") // Ok("hello world")
// Сегменты пути
uri.path_segments("/api/v1/users")
// ["api", "v1", "users"]
uri.percent_encode и uri.percent_decode кодируют/декодируют спецсимволы в компонентах URI, а uri.path_segments разбивает путь на отдельные сегменты — удобно для маршрутизации.
Origin и merge
// origin — схема + хост + порт
let assert Ok(u) = uri.parse("https://example.com:8080/path")
uri.origin(u) // Ok("https://example.com:8080")
// merge — разрешение относительного URI
let assert Ok(base) = uri.parse("https://example.com/a/b")
let assert Ok(relative) = uri.parse("../c")
uri.merge(base, relative)
// Ok(Uri(... path: "/c" ...))
uri.origin извлекает схему, хост и порт из URI — именно эту часть проверяют при CORS-запросах. uri.merge разрешает относительный URI относительно базового по правилам RFC 3986.
gleam/pair
Утилиты для работы с кортежами из двух элементов:
import gleam/pair
pair.first(#("hello", 42)) // "hello"
pair.second(#("hello", 42)) // 42
pair.swap(#("a", 1)) // #(1, "a")
pair.map_first(#("hello", 42), string.uppercase)
// #("HELLO", 42)
pair.map_second(#("hello", 42), fn(n) { n * 2 })
// #("hello", 84)
pair.new("key", "value") // #("key", "value")
pair удобен в пайплайнах, когда нужно трансформировать один элемент кортежа:
entries
|> list.map(pair.map_second(_, int.to_string))
Такой стиль позволяет избежать анонимных функций вида fn(pair) { #(pair.0, transform(pair.1)) } — функции pair.map_first и pair.map_second передаются в list.map напрямую.
Проект: HTML builder
Соберём изученные модули в проекте — мини-библиотека для генерации HTML. Это практичный пример: такие билдеры используются внутри веб-фреймворков (например, Wisp отдаёт string_tree как тело ответа).
import gleam/list
import gleam/string
import gleam/string_tree
/// Экранирование спецсимволов HTML.
/// Без этого строка вроде "<script>" станет рабочим тегом — это XSS-уязвимость.
pub fn escape_html(text: String) -> String {
text
|> string.replace("&", "&")
|> string.replace("<", "<")
|> string.replace(">", ">")
|> string.replace("\"", """)
|> string.replace("'", "'")
}
/// Рендер одного атрибута: ` key="value"` (с экранированием значения)
fn render_attr(attr: #(String, String)) -> String {
" " <> attr.0 <> "=\"" <> escape_html(attr.1) <> "\""
}
/// Создание HTML-тега с атрибутами и содержимым
pub fn tag(
name: String,
attrs: List(#(String, String)),
children: String,
) -> String {
let attrs_str =
attrs
|> list.map(render_attr)
|> string.concat
"<" <> name <> attrs_str <> ">" <> children <> "</" <> name <> ">"
}
/// Сборка нескольких элементов через string_tree — O(1) на каждый append
pub fn render(elements: List(String)) -> String {
elements
|> list.fold(string_tree.new(), fn(tree, el) {
string_tree.append(tree, el)
})
|> string_tree.to_string
}
Пример использования:
let page =
render([
tag("h1", [], "Привет, Gleam!"),
tag("p", [#("class", "intro")], "Это параграф."),
tag("a", [#("href", "https://gleam.run")], "Сайт Gleam"),
])
// "<h1>Привет, Gleam!</h1><p class=\"intro\">Это параграф.</p><a href=\"https://gleam.run\">Сайт Gleam</a>"
Проект демонстрирует:
string.replace— цепочка замен для экранированияlist.map+string.concat— сборка атрибутовstring_tree— эффективная конкатенация множества элементов- Безопасность: экранирование пользовательского ввода для защиты от XSS
В упражнениях вы расширите этот билдер: добавите списки, таблицы и автолинковку.
Упражнения
Все упражнения продолжают проект HTML builder — вы будете расширять его новыми возможностями.
Решения пишите в файле exercises/chapter06/test/my_solutions.gleam. Запускайте тесты:
cd exercises/chapter06
gleam test
Каждое упражнение добавляет новую функцию в HTML-билдер. Начните с первого и двигайтесь по порядку.
1. escape_html (Среднее)
Реализуйте экранирование HTML-спецсимволов. Это основа безопасного HTML — без экранирования пользовательский ввод вроде <script> превращается в рабочий код (XSS-атака).
pub fn escape_html(text: String) -> String
Нужно заменить 5 символов:
&→&(важно заменить первым, иначе сломаете остальные замены)<→<>→>"→"'→'
Примеры:
escape_html("<script>alert('xss')</script>")
// "<script>alert('xss')</script>"
escape_html("Tom & Jerry")
// "Tom & Jerry"
Подсказка: цепочка вызовов string.replace.
2. tag (Среднее)
Создайте HTML-тег с именем, атрибутами и содержимым. Значения атрибутов должны быть экранированы через вашу функцию escape_html.
pub fn tag(
name: String,
attrs: List(#(String, String)),
children: String,
) -> String
Примеры:
tag("p", [], "Hello")
// "<p>Hello</p>"
tag("a", [#("href", "https://gleam.run")], "Gleam")
// "<a href=\"https://gleam.run\">Gleam</a>"
tag("div", [#("class", "box"), #("id", "main")], "content")
// "<div class=\"box\" id=\"main\">content</div>"
Подсказка: для каждого атрибута #(key, value) сформируйте строку key="value", затем соедините через string.concat.
3. build_list (Среднее)
Постройте HTML-список из элементов. Параметр ordered определяет тип: <ol> или <ul>.
pub fn build_list(items: List(String), ordered: Bool) -> String
Примеры:
build_list(["яблоко", "банан", "вишня"], False)
// "<ul><li>яблоко</li><li>банан</li><li>вишня</li></ul>"
build_list(["первый", "второй"], True)
// "<ol><li>первый</li><li>второй</li></ol>"
Подсказка: оберните каждый элемент в <li>...</li>, соедините, оберните в <ul> или <ol>. Можете использовать вашу функцию tag.
4. linkify (Средне-сложное)
Найдите URL в тексте с помощью регулярного выражения и оберните их в <a> теги.
pub fn linkify(text: String) -> String
Считайте URL-ом любую подстроку, начинающуюся с https:// или http://, за которой следуют один или более непробельных символов.
Примеры:
linkify("Смотрите https://gleam.run — отличный язык")
// "Смотрите <a href=\"https://gleam.run\">https://gleam.run</a> — отличный язык"
linkify("нет ссылок тут")
// "нет ссылок тут"
linkify("два: http://a.com и https://b.org конец")
// "два: <a href=\"http://a.com\">http://a.com</a> и <a href=\"https://b.org\">https://b.org</a> конец"
Подсказка: используйте regexp.scan с паттерном https?://\\S+, чтобы найти все URL. Затем пройдите по совпадениям и замените каждый URL на <a href="...">...</a> через string.replace.
5. build_table (Сложное)
Постройте HTML-таблицу из заголовков и строк данных. Используйте string_tree для эффективной сборки.
pub fn build_table(
headers: List(String),
rows: List(List(String)),
) -> String
Примеры:
build_table(
["Имя", "Возраст"],
[["Алиса", "30"], ["Боб", "25"]],
)
// "<table><thead><tr><th>Имя</th><th>Возраст</th></tr></thead><tbody><tr><td>Алиса</td><td>30</td></tr><tr><td>Боб</td><td>25</td></tr></tbody></table>"
Подсказка: разбейте задачу на части — сначала функция для одной строки (<tr><td>...</td></tr>), затем соберите <thead> из заголовков (с <th>) и <tbody> из строк данных. Используйте string_tree и list.fold для сборки.
Заключение
В этой главе мы изучили:
- Строки — UTF-8, графемы, кодпоинты и богатый API
gleam/string - String builder (
gleam/string_tree) — эффективная конкатенация через IO-списки - Битовые массивы — бинарные данные,
<<>>синтаксис, base64/base16 - Pattern matching на bit arrays — мощный инструмент для парсинга бинарных протоколов
- Bytes builder (
gleam/bytes_tree) — эффективная сборка байтов - Regex (
gleam/regexp) — регулярные выражения для поиска и замены - URI (
gleam/uri) — парсинг и сборка URL - Pair (
gleam/pair) — утилиты для кортежей
В следующей главе мы перейдём к FFI, JSON и типобезопасному парсингу — научимся взаимодействовать с Erlang, обрабатывать JSON-данные и проектировать надёжные API через непрозрачные и фантомные типы.