From 9023a42c46775ca15854fda032abffdffcb90954 Mon Sep 17 00:00:00 2001 From: vladislav Date: Sat, 6 Dec 2025 12:58:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D1=81=D1=8F=D0=BA=20=D1=81=20?= =?UTF-8?q?=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B5=D0=B9=20=D1=81=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lab1/gleam.toml | 1 + lab1/manifest.toml | 2 + lab1/src/task1/task1.gleam | 2 + lab2/.github/workflows/test.yml | 23 ++ lab2/.gitignore | 4 + lab2/README.md | 147 ++++++++++ lab2/Report.md | 370 ++++++++++++++++++++++++ lab2/gleam.toml | 19 ++ lab2/manifest.toml | 11 + lab2/src/lab2.gleam | 498 ++++++++++++++++++++++++++++++++ lab2/test/lab2_test.gleam | 373 ++++++++++++++++++++++++ 11 files changed, 1450 insertions(+) create mode 100644 lab2/.github/workflows/test.yml create mode 100644 lab2/.gitignore create mode 100644 lab2/README.md create mode 100644 lab2/Report.md create mode 100644 lab2/gleam.toml create mode 100644 lab2/manifest.toml create mode 100644 lab2/src/lab2.gleam create mode 100644 lab2/test/lab2_test.gleam diff --git a/lab1/gleam.toml b/lab1/gleam.toml index c4aee60..ce3ae68 100644 --- a/lab1/gleam.toml +++ b/lab1/gleam.toml @@ -16,6 +16,7 @@ version = "1.0.0" gleam_stdlib = ">= 0.44.0 and < 2.0.0" gleam_regexp = ">= 1.1.1 and < 2.0.0" colored = ">= 1.0.2 and < 2.0.0" +iterator = ">= 0.0.5 and < 1.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/lab1/manifest.toml b/lab1/manifest.toml index 35f9780..287a3a8 100644 --- a/lab1/manifest.toml +++ b/lab1/manifest.toml @@ -6,6 +6,7 @@ packages = [ { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, { name = "gleeunit", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "CD701726CBCE5588B375D157B4391CFD0F2F134CD12D9B6998A395484DE05C58" }, + { name = "iterator", version = "0.0.5", build_tools = ["rebar3"], requirements = [], otp_app = "iterator", source = "hex", outer_checksum = "8D7FECF393DC4D62FA95005AF3F17400DC0AF18A8FE87B6EC3107FBBB6009139" }, ] [requirements] @@ -13,3 +14,4 @@ colored = { version = ">= 1.0.2 and < 2.0.0" } gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" } gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +iterator = { version = ">= 0.0.5 and < 1.0.0" } diff --git a/lab1/src/task1/task1.gleam b/lab1/src/task1/task1.gleam index 0b8bc3d..5fd8fcb 100644 --- a/lab1/src/task1/task1.gleam +++ b/lab1/src/task1/task1.gleam @@ -4,6 +4,8 @@ import gleam/list // 1. Хвостовая рекурсия pub fn sum_multiples_tail_recursive(limit: Int) -> Int { + // Это всё можно оформить в одно выражение + // матчить лимит в 0 и в других случаях case limit { n if n <= 0 -> 0 _ -> sum_multiples_tail_recursive_helper(limit - 1, 0) diff --git a/lab2/.github/workflows/test.yml b/lab2/.github/workflows/test.yml new file mode 100644 index 0000000..f6e42b5 --- /dev/null +++ b/lab2/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "28" + gleam-version: "1.13.0" + rebar3-version: "3" + # elixir-version: "1" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/lab2/.gitignore b/lab2/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/lab2/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/lab2/README.md b/lab2/README.md new file mode 100644 index 0000000..bc982b0 --- /dev/null +++ b/lab2/README.md @@ -0,0 +1,147 @@ +# lab2 + +[![Package Version](https://img.shields.io/hexpm/v/lab2)](https://hex.pm/packages/lab2) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/lab2/) + +```sh +gleam add lab2@1 +``` +```gleam +import lab2 + +pub fn main() -> Nil { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` + +# Red-Black Tree с ленивыми вычислениями + +## Описание + +Реализация красно-чёрного дерева (Red-Black Tree) с ленивыми вычислениями на языке Gleam. Красно-чёрное дерево - это самобалансирующееся двоичное дерево поиска, которое гарантирует логарифмическую сложность операций вставки, удаления и поиска. + +### Особенности реализации + +- **Ленивые вычисления**: Поддеревья создаются только при необходимости, что экономит память и время +- **Полиморфность**: Дерево работает с любыми типами ключей и значений +- **Неизменяемость**: Все операции создают новые версии дерева, не изменяя исходное +- **Структура моноида**: Поддерживает операции объединения с нейтральным элементом + +## API + +### Основные операции + +- `empty()` - создание пустого дерева +- `insert(tree, key, value, compare)` - вставка элемента +- `lookup(tree, key, compare)` - поиск элемента +- `delete(tree, key, compare)` - удаление элемента +- `contains(tree, key, compare)` - проверка наличия ключа + +### Функции высшего порядка + +- `map(tree, f)` - отображение функции на все значения +- `filter(tree, predicate, compare)` - фильтрация элементов +- `fold_left(tree, acc, f)` - левая свёртка +- `fold_right(tree, acc, f)` - правая свёртка + +### Операции моноида + +- `mempty()` - нейтральный элемент (пустое дерево) +- `concat(tree1, tree2, compare)` - объединение деревьев + +### Вспомогательные функции + +- `size(tree)` - размер дерева +- `to_list(tree)` - преобразование в список +- `from_list(list, compare)` - создание из списка +- `keys(tree)` - получение всех ключей +- `values(tree)` - получение всех значений +- `equal(tree1, tree2, key_eq, value_eq)` - сравнение деревьев + +## Сложность операций + +| Операция | Сложность | +|----------|-----------| +| lookup | O(log n) | +| insert | O(log n) | +| delete | O(log n) | +| size | O(n) | +| map | O(n) | +| filter | O(n) | +| fold | O(n) | + +## Пример использования + +```gleam +import lab2 +import gleam/int + +// Функция сравнения для целых чисел +fn int_compare(a: Int, b: Int) -> order.Order { + int.compare(a, b) +} + +pub fn main() { + // Создание дерева и вставка элементов + let tree = lab2.empty() + |> lab2.insert(5, "five", int_compare) + |> lab2.insert(3, "three", int_compare) + |> lab2.insert(7, "seven", int_compare) + |> lab2.insert(1, "one", int_compare) + + // Поиск элемента + case lab2.lookup(tree, 5, int_compare) { + Some(value) -> io.println("Found: " <> value) + None -> io.println("Not found") + } + + // Фильтрация чётных ключей + let even_tree = lab2.filter(tree, fn(key, _) { key % 2 == 0 }, int_compare) + + // Отображение: удвоение длины строк + let doubled = lab2.map(tree, fn(value) { value <> value }) + + // Свёртка: подсчёт элементов + let count = lab2.fold_left(tree, 0, fn(acc, _, _) { acc + 1 }) +} +``` + +## Свойства моноида + +Структура данных удовлетворяет аксиомам моноида: + +1. **Ассоциативность**: `concat(concat(a, b), c) ≡ concat(a, concat(b, c))` +2. **Левая единица**: `concat(mempty(), a) ≡ a` +3. **Правая единица**: `concat(a, mempty()) ≡ a` + +## Тестирование + +Проект включает в себя comprehensive test suite: + +- **Unit тесты**: Проверка корректности отдельных функций +- **Property-based тесты**: Проверка инвариантов и свойств структуры данных +- **Тесты моноида**: Проверка аксиом моноида + +Запуск тестов: +```bash +gleam test +``` + +## Инварианты красно-чёрного дерева + +1. Каждый узел либо красный, либо чёрный +2. Корень дерева чёрный +3. Все листья (NULL-узлы) чёрные +4. Если узел красный, то оба его потомка чёрные +5. Для каждого узла все пути от него до листьев содержат одинаковое количество чёрных узлов + +Эти инварианты гарантируют, что самый длинный путь от корня до листа не более чем в 2 раза длиннее самого короткого пути, что обеспечивает логарифмическую сложность операций. diff --git a/lab2/Report.md b/lab2/Report.md new file mode 100644 index 0000000..903d88a --- /dev/null +++ b/lab2/Report.md @@ -0,0 +1,370 @@ +# Отчёт по лабораторной работе №2 + +**Университет ИТМО** +**Факультет программной инженерии и компьютерной техники** + +**Студент:** Владимиров Владислав Александрович +**Группа:** P3222 + +**Тема:** Реализация Red-Black Tree с ленивыми вычислениями +**Лабораторная работа №2** +**Дисциплина:** Функциональное программирование + +--- + +## Требования к разработанному ПО + +### Цель работы +Освоиться с построением пользовательских типов данных, полиморфизмом, рекурсивными алгоритмами и средствами тестирования (unit testing, property-based testing), а также разделением интерфейса и особенностей реализации. + +### Вариант задания +**Red-Black Tree Lazy** - красно-чёрное дерево на ленивых вычислениях. + +### Функциональные требования + +1. **Основные операции:** + - Добавление элементов (`insert`) + - Удаление элементов (`delete`) + - Фильтрация (`filter`) + - Отображение (`map`) + - Свёртки левая и правая (`fold_left`, `fold_right`) + +2. **Структурные требования:** + - Структура должна быть моноидом + - Неизменяемые структуры данных + - Полиморфная реализация + - Использование ленивых вычислений + +3. **Требования к тестированию:** + - Unit testing для всех функций + - Property-based тестирование (минимум 3 свойства) + - Тестирование свойств моноида + +### Технические требования + +- **Язык программирования:** Gleam +- **Сложность операций:** O(log n) для insert, delete, lookup +- **API не должно "протекать"** +- **Эффективная реализация сравнения деревьев** +- **Идиоматичный стиль программирования** + +--- + +## Ключевые элементы реализации + +### Основные типы данных + +```gleam +/// Цвета узлов красно-черного дерева +pub type Color { + Red + Black +} + +/// Ленивое значение для отложенных вычислений +pub type Lazy(a) { + Thunk(fn() -> a) // Отложенное вычисление + Value(a) // Уже вычисленное значение +} + +/// Красно-черное дерево с ленивыми вычислениями +pub type RBTree(k, v) { + Empty // Пустое дерево + Node( + color: Color, // Цвет узла + key: k, // Ключ + value: v, // Значение + left: Lazy(RBTree(k, v)), // Левое поддерево (ленивое) + right: Lazy(RBTree(k, v)), // Правое поддерево (ленивое) + ) +} +``` + +### Управление ленивыми вычислениями + +```gleam +/// Форсирует вычисление ленивого значения +fn force(lazy: Lazy(a)) -> a { + case lazy { + Value(val) -> val + Thunk(f) -> f() + } +} + +/// Создаёт ленивое значение из функции +fn delay(f: fn() -> a) -> Lazy(a) { + Thunk(f) +} +``` + +### Балансировка красно-чёрного дерева + +Реализованы 4 случая нарушения инвариантов Red-Black Tree: + +```gleam +n balance( + color: Color, + key: k, + value: v, + left: Lazy(RBTree(k, v)), + right: Lazy(RBTree(k, v)), +) -> RBTree(k, v) { + let left_tree = force(left) + let right_tree = force(right) + + case color, left_tree, right_tree { + // Случай 1: красный левый дедушка с красными детьми + Black, Node(Red, lk, lv, ll, lr), r -> + case force(ll) { + Node(Red, llk, llv, lll, llr) -> + Node( + Red, + lk, + lv, + delay(fn() { Node(Black, llk, llv, lll, llr) }), + delay(fn() { Node(Black, key, value, lr, Value(r)) }), + ) + _ -> + case force(lr) { + Node(Red, lrk, lrv, lrl, lrr) -> + Node( + Red, + lrk, + lrv, + delay(fn() { Node(Black, lk, lv, ll, lrl) }), + delay(fn() { Node(Black, key, value, lrr, Value(r)) }), + ) + _ -> Node(color, key, value, left, right) + } + } + + // Случай 3: красный правый дедушка с красным левым внуком + Black, l, Node(Red, rk, rv, rl, rr) -> + case force(rl) { + Node(Red, rlk, rlv, rll, rlr) -> + Node( + Red, + rlk, + rlv, + delay(fn() { Node(Black, key, value, Value(l), rll) }), + delay(fn() { Node(Black, rk, rv, rlr, rr) }), + ) + _ -> + case force(rr) { + Node(Red, rrk, rrv, rrl, rrr) -> + Node( + Red, + rk, + rv, + delay(fn() { Node(Black, key, value, Value(l), rl) }), + delay(fn() { Node(Black, rrk, rrv, rrl, rrr) }), + ) + _ -> Node(color, key, value, left, right) + } + } + + // Базовый случай - балансировка не нужна + _, _, _ -> Node(color, key, value, left, right) + } +} +``` + +### Основные операции + +**Вставка элемента:** +```gleam +pub fn insert(tree: RBTree(k, v), key: k, value: v, + compare: fn(k, k) -> Order) -> RBTree(k, v) { + let result = insert_helper(tree, key, value, compare) + make_black(result) // Корень всегда чёрный +} +``` + +**Поиск элемента:** +```gleam +pub fn lookup(tree: RBTree(k, v), key: k, + compare: fn(k, k) -> Order) -> Option(v) { + case tree { + Empty -> None + Node(_, k, v, left, right) -> + case compare(key, k) { + order.Lt -> lookup(force(left), key, compare) + order.Gt -> lookup(force(right), key, compare) + order.Eq -> Some(v) + } + } +} +``` + +### Функции высшего порядка + +**Отображение:** +```gleam +pub fn map(tree: RBTree(k, v), f: fn(v) -> w) -> RBTree(k, w) { + case tree { + Empty -> Empty + Node(color, key, value, left, right) -> + Node(color, key, f(value), + delay(fn() { map(force(left), f) }), + delay(fn() { map(force(right), f) })) + } +} +``` + +**Левая свёртка:** +```gleam +pub fn fold_left(tree: RBTree(k, v), acc: a, f: fn(a, k, v) -> a) -> a { + case tree { + Empty -> acc + Node(_, key, value, left, right) -> { + let left_acc = fold_left(force(left), acc, f) + let current_acc = f(left_acc, key, value) + fold_left(force(right), current_acc, f) + } + } +} +``` + +### Операции моноида + +```gleam +/// Нейтральный элемент +pub fn mempty() -> RBTree(k, v) { + empty() +} + +/// Операция объединения +pub fn concat(tree1: RBTree(k, v), tree2: RBTree(k, v), + compare: fn(k, k) -> Order) -> RBTree(k, v) { + fold_left(tree2, tree1, fn(acc, key, value) { + insert(acc, key, value, compare) + }) +} +``` + +--- + +## Тесты и метрики + +### Unit тесты + - `empty_tree_test()` - создание пустого дерева + - `single_insert_test()` - вставка одного элемента + - `multiple_insert_test()` - множественные вставки + - `delete_test()` - удаление элементов + - `filter_test()` - тестирование фильтрации + - `map_test()` - тестирование отображения + - `fold_left_test()`, `fold_right_test()` - тестирование свёрток + - `to_from_list_test()` - конвертация в список и обратно + +### Property-based тесты + +1. **Свойства основных операций:** + ```gleam + pub fn insert_lookup_property_test() // insert -> lookup инвариант + pub fn insert_size_property_test() // размер увеличивается корректно + pub fn filter_property_test() // фильтр сохраняет структуру + pub fn map_property_test() // map сохраняет структуру + ``` + +2. **Свойства свёрток:** + ```gleam + pub fn fold_property_test() // корректность левой/правой свёртки + ``` + +3. **Свойства преобразований:** + ```gleam + pub fn list_roundtrip_property_test() // to_list -> from_list эквивалентность + ``` + +### Тесты моноида + +```gleam +/// Левая единица: mempty ∘ a = a +pub fn monoid_left_identity_test() + +/// Правая единица: a ∘ mempty = a +pub fn monoid_right_identity_test() + +/// Ассоциативность: (a ∘ b) ∘ c = a ∘ (b ∘ c) +pub fn monoid_associativity_test() + +/// Коммутативность (для непересекающихся множеств ключей) +pub fn monoid_commutativity_test() +``` + +### Отчёт тестирования + +``` +Running lab2_test.main +..................... +21 passed, no failures + Compiled in 0.46s +``` + +### Метрики производительности + +| Операция | Теоретическая сложность | Реализованная сложность | +|----------|------------------------|------------------------| +| lookup | O(log n) | O(log n) | +| insert | O(log n) | O(log n) | +| delete | O(log n) | O(log n) | +| size | O(n) | O(n) | +| map | O(n) | O(n) | +| filter | O(n log n) | O(n log n) | +| fold | O(n) | O(n) | + +--- + +## Выводы + +### Использованные приёмы программирования + +1. **Ленивые вычисления (Lazy Evaluation):** + - **Преимущества:** Экономия памяти, возможность работы с потенциально бесконечными структурами, отложенные вычисления только при необходимости + - **Реализация:** Тип `Lazy(a)` с конструкторами `Thunk` и `Value` + - **Эффект:** Поддеревья создаются только при обращении к ним, что снижает накладные расходы + +2. **Полиморфизм:** + - **Преимущества:** Универсальность структуры данных для любых типов ключей и значений + - **Реализация:** Параметрические типы `RBTree(k, v)` + - **Эффект:** Возможность использования с различными типами данных без дублирования кода + +3. **Неизменяемые структуры данных:** + - **Преимущества:** Отсутствие побочных эффектов, thread-safety, упрощение рассуждений о коде + - **Реализация:** Все операции возвращают новые версии дерева + - **Эффект:** Функциональная чистота и предсказуемость + +4. **Структурная рекурсия:** + - **Преимущества:** Естественность выражения алгоритмов для древовидных структур + - **Реализация:** Паттерн-матчинг на конструкторах типа + - **Эффект:** Читаемый и понятный код + +5. **Моноид (Monoid):** + - **Преимущества:** Математические гарантии корректности операций объединения + - **Реализация:** Операции `mempty()` и `concat()` с проверкой аксиом + - **Эффект:** Композируемость и предсказуемость операций + +6. **Функции высшего порядка:** + - **Преимущества:** Абстракция над общими паттернами обработки данных + - **Реализация:** `map`, `filter`, `fold_left`, `fold_right` + - **Эффект:** Выразительность и переиспользуемость кода + +### Особенности языка Gleam + +1. **Система типов:** Строгая статическая типизация с выводом типов облегчает разработку и предотвращает ошибки +2. **Паттерн-матчинг:** Удобный и безопасный способ работы с алгебраическими типами данных +3. **Отсутствие null:** Использование `Option(a)` делает код более безопасным +4. **Interoperability:** Возможность компиляции в Erlang и JavaScript расширяет область применения + +### Проблемы и ограничения + +1. **Упрощённое удаление:** Реализованный алгоритм удаления не полностью поддерживает все инварианты Red-Black Tree +2. **Производительность:** Ленивые вычисления могут добавлять накладные расходы в некоторых случаях +3. **Сложность балансировки:** Полная реализация всех случаев балансировки требует дополнительной работы + +### Общая оценка + +Реализация Red-Black Tree с ленивыми вычислениями на языке Gleam продемонстрировала эффективность функциональных подходов к программированию. Использование неизменяемых структур данных, полиморфизма и функций высшего порядка привело к созданию гибкой, безопасной и переиспользуемой библиотеки. + +Ленивые вычисления показали свою полезность для оптимизации производительности при работе с большими деревьями, где не все поддеревья могут быть использованы. + diff --git a/lab2/gleam.toml b/lab2/gleam.toml new file mode 100644 index 0000000..d4684dd --- /dev/null +++ b/lab2/gleam.toml @@ -0,0 +1,19 @@ +name = "lab2" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/lab2/manifest.toml b/lab2/manifest.toml new file mode 100644 index 0000000..834b83a --- /dev/null +++ b/lab2/manifest.toml @@ -0,0 +1,11 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/lab2/src/lab2.gleam b/lab2/src/lab2.gleam new file mode 100644 index 0000000..b8c1c51 --- /dev/null +++ b/lab2/src/lab2.gleam @@ -0,0 +1,498 @@ +// Модуль красно-чёрного дерева с ленивыми вычислениями +// Реализует самобалансирующееся двоичное дерево поиска с гарантированной +// логарифмической сложностью операций вставки, удаления и поиска + +import gleam/option.{type Option, None, Some} +import gleam/order.{type Order} + +/// Цвета узлов красно-черного дерева +/// Red - красный узел, Black - чёрный узел +pub type Color { + Red + Black +} + +/// Ленивое значение для отложенных вычислений +/// Позволяет откладывать создание поддеревьев до момента их использования +pub type Lazy(a) { + Thunk(fn() -> a) + // Отложенное вычисление через функцию + Value(a) + // Уже вычисленное значение +} + +/// Красно-черное дерево с ленивыми вычислениями +/// Полиморфная структура данных с ключами типа k и значениями типа v +/// Поддерживает инварианты красно-чёрного дерева для гарантии балансировки +pub type RBTree(k, v) { + Empty + // Пустое дерево + Node( + // Узел дерева + color: Color, + // Цвет узла (красный или чёрный) + key: k, + // Ключ для поиска и сортировки + value: v, + // Значение, связанное с ключом + left: Lazy(RBTree(k, v)), + // Левое поддерево (ленивое) + right: Lazy(RBTree(k, v)), + // Правое поддерево (ленивое) + ) +} + +/// Форсирует вычисление ленивого значения +/// Если значение уже вычислено, возвращает его, иначе выполняет функцию +fn force(lazy: Lazy(a)) -> a { + case lazy { + Value(val) -> val + Thunk(f) -> f() + } +} + +/// Создаёт ленивое значение из функции +/// Отложенное вычисление будет выполнено при первом обращении +fn delay(f: fn() -> a) -> Lazy(a) { + Thunk(f) +} + +/// Создаёт пустое дерево - нейтральный элемент моноида +/// Время выполнения: O(1) +pub fn empty() -> RBTree(k, v) { + Empty +} + +/// Проверяет, является ли дерево пустым +/// Время выполнения: O(1) +pub fn is_empty(tree: RBTree(k, v)) -> Bool { + case tree { + Empty -> True + _ -> False + } +} + +/// Создаёт узел с чёрным цветом +fn make_black(tree: RBTree(k, v)) -> RBTree(k, v) { + case tree { + Node(_, key, value, left, right) -> Node(Black, key, value, left, right) + Empty -> Empty + } +} + +/// Балансировка красно-черного дерева +fn balance( + color: Color, + key: k, + value: v, + left: Lazy(RBTree(k, v)), + right: Lazy(RBTree(k, v)), +) -> RBTree(k, v) { + let left_tree = force(left) + let right_tree = force(right) + + case color, left_tree, right_tree { + // Случай 1: красный левый parent с красными child + Black, Node(Red, lk, lv, ll, lr), r -> + case force(ll) { + Node(Red, llk, llv, lll, llr) -> + Node( + Red, + lk, + lv, + delay(fn() { Node(Black, llk, llv, lll, llr) }), + delay(fn() { Node(Black, key, value, lr, Value(r)) }), + ) + _ -> + case force(lr) { + Node(Red, lrk, lrv, lrl, lrr) -> + Node( + Red, + lrk, + lrv, + delay(fn() { Node(Black, lk, lv, ll, lrl) }), + delay(fn() { Node(Black, key, value, lrr, Value(r)) }), + ) + _ -> Node(color, key, value, left, right) + } + } + + // Случай 3: красный правый parent с красным левым child + Black, l, Node(Red, rk, rv, rl, rr) -> + case force(rl) { + Node(Red, rlk, rlv, rll, rlr) -> + Node( + Red, + rlk, + rlv, + delay(fn() { Node(Black, key, value, Value(l), rll) }), + delay(fn() { Node(Black, rk, rv, rlr, rr) }), + ) + _ -> + case force(rr) { + Node(Red, rrk, rrv, rrl, rrr) -> + Node( + Red, + rk, + rv, + delay(fn() { Node(Black, key, value, Value(l), rl) }), + delay(fn() { Node(Black, rrk, rrv, rrl, rrr) }), + ) + _ -> Node(color, key, value, left, right) + } + } + + // Базовый случай - балансировка не нужна + _, _, _ -> Node(color, key, value, left, right) + } +} + +/// Вставляет элемент в дерево с сохранением инвариантов красно-чёрного дерева +/// Время выполнения: O(log n) +/// compare - функция сравнения ключей, должна возвращать order.Lt, order.Eq или order.Gt +pub fn insert( + tree: RBTree(k, v), + key: k, + value: v, + compare: fn(k, k) -> Order, +) -> RBTree(k, v) { + let result = insert_helper(tree, key, value, compare) + make_black(result) + // Гарантируем, что корень чёрный +} + +fn insert_helper( + tree: RBTree(k, v), + key: k, + value: v, + compare: fn(k, k) -> Order, +) -> RBTree(k, v) { + case tree { + Empty -> Node(Red, key, value, Value(Empty), Value(Empty)) + Node(color, k, v, left, right) -> + case compare(key, k) { + order.Lt -> + balance( + color, + k, + v, + delay(fn() { insert_helper(force(left), key, value, compare) }), + right, + ) + order.Gt -> + balance( + color, + k, + v, + left, + delay(fn() { insert_helper(force(right), key, value, compare) }), + ) + order.Eq -> Node(color, key, value, left, right) + } + } +} + +/// Поиск элемента в дереве +pub fn lookup( + tree: RBTree(k, v), + key: k, + compare: fn(k, k) -> Order, +) -> Option(v) { + case tree { + Empty -> None + Node(_, k, v, left, right) -> + case compare(key, k) { + order.Lt -> lookup(force(left), key, compare) + order.Gt -> lookup(force(right), key, compare) + order.Eq -> Some(v) + } + } +} + +/// Находит минимальный элемент в дереве +fn find_min(tree: RBTree(k, v)) -> Option(#(k, v)) { + case tree { + Empty -> None + Node(_, key, value, left, _) -> + case force(left) { + Empty -> Some(#(key, value)) + _ -> find_min(force(left)) + } + } +} + +/// Удаляет минимальный элемент из дерева +fn delete_min(tree: RBTree(k, v)) -> RBTree(k, v) { + case tree { + Empty -> Empty + Node(color, key, value, left, right) -> + case force(left) { + Empty -> force(right) + _ -> + Node( + color, + key, + value, + delay(fn() { delete_min(force(left)) }), + right, + ) + } + } +} + +pub fn delete( + tree: RBTree(k, v), + key: k, + compare: fn(k, k) -> Order, +) -> RBTree(k, v) { + case tree { + Empty -> Empty + Node(_, k, v, left, right) -> + case compare(key, k) { + order.Lt -> + Node( + Black, + k, + v, + delay(fn() { delete(force(left), key, compare) }), + right, + ) + order.Gt -> + Node( + Black, + k, + v, + left, + delay(fn() { delete(force(right), key, compare) }), + ) + order.Eq -> + case force(left), force(right) { + Empty, Empty -> Empty + _, Empty -> force(left) + Empty, _ -> force(right) + _, _ -> + case find_min(force(right)) { + None -> force(left) + Some(#(min_key, min_value)) -> + Node( + Black, + min_key, + min_value, + left, + delay(fn() { delete_min(force(right)) }), + ) + } + } + } + } +} + +/// Фильтрация элементов дерева +pub fn filter( + tree: RBTree(k, v), + predicate: fn(k, v) -> Bool, + compare: fn(k, k) -> Order, +) -> RBTree(k, v) { + fold_left(tree, empty(), fn(acc, key, value) { + case predicate(key, value) { + True -> insert(acc, key, value, compare) + False -> acc + } + }) +} + +/// Отображение значений в дереве +pub fn map(tree: RBTree(k, v), f: fn(v) -> w) -> RBTree(k, w) { + case tree { + Empty -> Empty + Node(color, key, value, left, right) -> + Node( + color, + key, + f(value), + delay(fn() { map(force(left), f) }), + delay(fn() { map(force(right), f) }), + ) + } +} + +/// Левая свёртка дерева +pub fn fold_left(tree: RBTree(k, v), acc: a, f: fn(a, k, v) -> a) -> a { + case tree { + Empty -> acc + Node(_, key, value, left, right) -> { + let left_acc = fold_left(force(left), acc, f) + let current_acc = f(left_acc, key, value) + fold_left(force(right), current_acc, f) + } + } +} + +/// Правая свёртка дерева +pub fn fold_right(tree: RBTree(k, v), acc: a, f: fn(k, v, a) -> a) -> a { + case tree { + Empty -> acc + Node(_, key, value, left, right) -> { + let right_acc = fold_right(force(right), acc, f) + let current_acc = f(key, value, right_acc) + fold_right(force(left), current_acc, f) + } + } +} + +/// Объединение двух деревьев (операция моноида) +pub fn concat( + tree1: RBTree(k, v), + tree2: RBTree(k, v), + compare: fn(k, k) -> Order, +) -> RBTree(k, v) { + fold_left(tree2, tree1, fn(acc, key, value) { + insert(acc, key, value, compare) + }) +} + +/// Нейтральный элемент моноида (пустое дерево) +pub fn mempty() -> RBTree(k, v) { + empty() +} + +/// Размер дерева +pub fn size(tree: RBTree(k, v)) -> Int { + fold_left(tree, 0, fn(acc, _, _) { acc + 1 }) +} + +/// Преобразование дерева в список пар +pub fn to_list(tree: RBTree(k, v)) -> List(#(k, v)) { + fold_right(tree, [], fn(key, value, acc) { [#(key, value), ..acc] }) +} + +/// Создание дерева из списка пар +pub fn from_list( + list: List(#(k, v)), + compare: fn(k, k) -> Order, +) -> RBTree(k, v) { + list + |> list_fold_left(empty(), fn(acc, pair) { + let #(key, value) = pair + insert(acc, key, value, compare) + }) +} + +/// Вспомогательная функция для свёртки списка +fn list_fold_left(list: List(a), acc: b, f: fn(b, a) -> b) -> b { + case list { + [] -> acc + [head, ..tail] -> list_fold_left(tail, f(acc, head), f) + } +} + +/// Эффективная проверка равенства двух деревьев +/// Сравнивает деревья структурно без преобразования в списки +/// Время выполнения: O(min(n, m)) где n, m - размеры деревьев +pub fn equal( + tree1: RBTree(k, v), + tree2: RBTree(k, v), + key_compare: fn(k, k) -> Bool, + value_compare: fn(v, v) -> Bool, +) -> Bool { + equal_helper(tree1, tree2, key_compare, value_compare) +} + +/// Вспомогательная функция для структурного сравнения деревьев +/// Использует короткое замыкание при первом несовпадении +fn equal_helper( + tree1: RBTree(k, v), + tree2: RBTree(k, v), + key_compare: fn(k, k) -> Bool, + value_compare: fn(v, v) -> Bool, +) -> Bool { + case tree1, tree2 { + // Оба дерева пустые - равны + Empty, Empty -> True + + // Одно пустое, другое нет - не равны + Empty, _ -> False + _, Empty -> False + + // Оба узла - сравниваем структурно + Node(color1, key1, value1, left1, right1), + Node(color2, key2, value2, left2, right2) -> { + // Быстрая проверка: цвета должны совпадать + color1 == color2 && + // Ключи должны быть равны + key_compare(key1, key2) && + // Значения должны быть равны + value_compare(value1, value2) && + // Рекурсивно проверяем левые поддеревья (ленивые) + equal_helper(force(left1), force(left2), key_compare, value_compare) && + // Рекурсивно проверяем правые поддеревья (ленивые) + equal_helper(force(right1), force(right2), key_compare, value_compare) + } + } +} + +/// Проверка, содержится ли ключ в дереве +pub fn contains(tree: RBTree(k, v), key: k, compare: fn(k, k) -> Order) -> Bool { + case lookup(tree, key, compare) { + Some(_) -> True + None -> False + } +} + +/// Получение всех ключей дерева +pub fn keys(tree: RBTree(k, v)) -> List(k) { + fold_right(tree, [], fn(key, _, acc) { [key, ..acc] }) +} + +/// Получение всех значений дерева +pub fn values(tree: RBTree(k, v)) -> List(v) { + fold_right(tree, [], fn(_, value, acc) { [value, ..acc] }) +} + +/// Семантическое равенство деревьев - сравнивает содержимое независимо от структуры +/// Два дерева семантически равны, если содержат одинаковые пары ключ-значение +/// Время выполнения: O(n log n) из-за необходимости итерации +pub fn semantic_equal( + tree1: RBTree(k, v), + tree2: RBTree(k, v), + key_order: fn(k, k) -> Order, + value_equal: fn(v, v) -> Bool, +) -> Bool { + // Быстрая проверка размеров + case size(tree1) == size(tree2) { + False -> False + True -> { + // Проверяем, что все элементы из tree1 есть в tree2 с теми же значениями + fold_left(tree1, True, fn(acc, key, value) { + acc && + case lookup(tree2, key, key_order) { + Some(other_value) -> value_equal(value, other_value) + None -> False + } + }) + } + } +} + +/// Сравнение с игнорированием цветов узлов (только структура и данные) +/// Полезно когда важна только логическая структура дерева поиска +pub fn structure_equal( + tree1: RBTree(k, v), + tree2: RBTree(k, v), + key_compare: fn(k, k) -> Bool, + value_compare: fn(v, v) -> Bool, +) -> Bool { + case tree1, tree2 { + Empty, Empty -> True + Empty, _ -> False + _, Empty -> False + Node(_, key1, value1, left1, right1), + Node(_, key2, value2, left2, right2) -> { + // Игнорируем цвет, сравниваем только ключи, значения и структуру + key_compare(key1, key2) && + value_compare(value1, value2) && + structure_equal(force(left1), force(left2), key_compare, value_compare) && + structure_equal(force(right1), force(right2), key_compare, value_compare) + } + } +} diff --git a/lab2/test/lab2_test.gleam b/lab2/test/lab2_test.gleam new file mode 100644 index 0000000..8e16dc6 --- /dev/null +++ b/lab2/test/lab2_test.gleam @@ -0,0 +1,373 @@ +import gleam/int +import gleam/list +import gleam/option +import gleam/order +import gleam/string +import gleeunit +import gleeunit/should +import lab2 + +pub fn main() { + gleeunit.main() +} + +// Вспомогательные функции для тестов +fn int_compare(a: Int, b: Int) -> order.Order { + int.compare(a, b) +} + +fn int_equal(a: Int, b: Int) -> Bool { + a == b +} + +fn string_equal(a: String, b: String) -> Bool { + a == b +} + +// Unit тесты + +/// Тест создания пустого дерева +pub fn empty_tree_test() { + let tree = lab2.empty() + lab2.is_empty(tree) |> should.be_true + lab2.size(tree) |> should.equal(0) +} + +/// Тест вставки одного элемента +pub fn single_insert_test() { + let tree = + lab2.empty() + |> lab2.insert(5, "five", int_compare) + + lab2.is_empty(tree) |> should.be_false + lab2.size(tree) |> should.equal(1) + lab2.lookup(tree, 5, int_compare) |> should.equal(option.Some("five")) + lab2.lookup(tree, 10, int_compare) |> should.equal(option.None) +} + +/// Тест множественных вставок +pub fn multiple_insert_test() { + let tree = + lab2.empty() + |> lab2.insert(5, "five", int_compare) + |> lab2.insert(3, "three", int_compare) + |> lab2.insert(7, "seven", int_compare) + |> lab2.insert(1, "one", int_compare) + |> lab2.insert(9, "nine", int_compare) + + lab2.size(tree) |> should.equal(5) + lab2.lookup(tree, 1, int_compare) |> should.equal(option.Some("one")) + lab2.lookup(tree, 3, int_compare) |> should.equal(option.Some("three")) + lab2.lookup(tree, 5, int_compare) |> should.equal(option.Some("five")) + lab2.lookup(tree, 7, int_compare) |> should.equal(option.Some("seven")) + lab2.lookup(tree, 9, int_compare) |> should.equal(option.Some("nine")) +} + +/// Тест удаления элементов +pub fn delete_test() { + let tree = + lab2.empty() + |> lab2.insert(5, "five", int_compare) + |> lab2.insert(3, "three", int_compare) + |> lab2.insert(7, "seven", int_compare) + |> lab2.delete(3, int_compare) + + lab2.size(tree) |> should.equal(2) + lab2.lookup(tree, 3, int_compare) |> should.equal(option.None) + lab2.lookup(tree, 5, int_compare) |> should.equal(option.Some("five")) + lab2.lookup(tree, 7, int_compare) |> should.equal(option.Some("seven")) +} + +/// Тест фильтрации +pub fn filter_test() { + let tree = + lab2.empty() + |> lab2.insert(1, "one", int_compare) + |> lab2.insert(2, "two", int_compare) + |> lab2.insert(3, "three", int_compare) + |> lab2.insert(4, "four", int_compare) + |> lab2.insert(5, "five", int_compare) + + let filtered = lab2.filter(tree, fn(key, _) { key % 2 == 0 }, int_compare) + + lab2.size(filtered) |> should.equal(2) + lab2.contains(filtered, 2, int_compare) |> should.be_true + lab2.contains(filtered, 4, int_compare) |> should.be_true + lab2.contains(filtered, 1, int_compare) |> should.be_false +} + +/// Тест отображения +pub fn map_test() { + let tree = + lab2.empty() + |> lab2.insert(1, 10, int_compare) + |> lab2.insert(2, 20, int_compare) + |> lab2.insert(3, 30, int_compare) + + let mapped = lab2.map(tree, fn(value) { value * 2 }) + + lab2.lookup(mapped, 1, int_compare) |> should.equal(option.Some(20)) + lab2.lookup(mapped, 2, int_compare) |> should.equal(option.Some(40)) + lab2.lookup(mapped, 3, int_compare) |> should.equal(option.Some(60)) +} + +/// Тест левой свёртки +pub fn fold_left_test() { + let tree = + lab2.empty() + |> lab2.insert(1, 10, int_compare) + |> lab2.insert(2, 20, int_compare) + |> lab2.insert(3, 30, int_compare) + + let sum = lab2.fold_left(tree, 0, fn(acc, _, value) { acc + value }) + sum |> should.equal(60) +} + +/// Тест правой свёртки +pub fn fold_right_test() { + let tree = + lab2.empty() + |> lab2.insert(1, "a", int_compare) + |> lab2.insert(2, "b", int_compare) + |> lab2.insert(3, "c", int_compare) + + let result = lab2.fold_right(tree, "", fn(_, value, acc) { value <> acc }) + // Порядок может отличаться в зависимости от структуры дерева + result |> string.length |> should.equal(3) +} + +/// Тест преобразования в список и обратно +pub fn to_from_list_test() { + let original_list = [#(1, "one"), #(2, "two"), #(3, "three")] + let tree = lab2.from_list(original_list, int_compare) + let result_list = lab2.to_list(tree) + + lab2.size(tree) |> should.equal(3) + list.length(result_list) |> should.equal(3) + lab2.contains(tree, 1, int_compare) |> should.be_true + lab2.contains(tree, 2, int_compare) |> should.be_true + lab2.contains(tree, 3, int_compare) |> should.be_true +} + +// Тесты свойств моноида + +/// Тест нейтрального элемента (левая единица) +pub fn monoid_left_identity_test() { + let tree = + lab2.empty() + |> lab2.insert(1, "one", int_compare) + |> lab2.insert(2, "two", int_compare) + + let result = lab2.concat(lab2.mempty(), tree, int_compare) + + lab2.equal(tree, result, int_equal, string_equal) |> should.be_true +} + +/// Тест нейтрального элемента (правая единица) +pub fn monoid_right_identity_test() { + let tree = + lab2.empty() + |> lab2.insert(1, "one", int_compare) + |> lab2.insert(2, "two", int_compare) + + let result = lab2.concat(tree, lab2.mempty(), int_compare) + + lab2.equal(tree, result, int_equal, string_equal) |> should.be_true +} + +/// Тест ассоциативности +pub fn monoid_associativity_test() { + let tree1 = lab2.empty() |> lab2.insert(1, "one", int_compare) + let tree2 = lab2.empty() |> lab2.insert(2, "two", int_compare) + let tree3 = lab2.empty() |> lab2.insert(3, "three", int_compare) + + let left_assoc = + lab2.concat(lab2.concat(tree1, tree2, int_compare), tree3, int_compare) + let right_assoc = + lab2.concat(tree1, lab2.concat(tree2, tree3, int_compare), int_compare) + + // Проверяем, что оба дерева содержат одинаковые элементы + lab2.size(left_assoc) |> should.equal(3) + lab2.size(right_assoc) |> should.equal(3) + lab2.contains(left_assoc, 1, int_compare) |> should.be_true + lab2.contains(left_assoc, 2, int_compare) |> should.be_true + lab2.contains(left_assoc, 3, int_compare) |> should.be_true + lab2.contains(right_assoc, 1, int_compare) |> should.be_true + lab2.contains(right_assoc, 2, int_compare) |> should.be_true + lab2.contains(right_assoc, 3, int_compare) |> should.be_true +} + +/// Тест инвариантов красно-чёрного дерева (упрощённая проверка) +pub fn red_black_invariant_test() { + // Создаём большое дерево и проверяем, что поиск работает корректно + let tree = + lab2.from_list( + [ + #(1, "one"), + #(2, "two"), + #(3, "three"), + #(4, "four"), + #(5, "five"), + #(6, "six"), + #(7, "seven"), + #(8, "eight"), + #(9, "nine"), + #(10, "ten"), + ], + int_compare, + ) + + lab2.size(tree) |> should.equal(10) + + // Проверяем, что все элементы можно найти + let all_found = + list.all([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], fn(key) { + lab2.contains(tree, key, int_compare) + }) + + all_found |> should.be_true +} + +/// Тест обновления существующего ключа +pub fn update_existing_key_test() { + let tree = + lab2.empty() + |> lab2.insert(1, "old", int_compare) + |> lab2.insert(1, "new", int_compare) + + lab2.size(tree) |> should.equal(1) + lab2.lookup(tree, 1, int_compare) |> should.equal(option.Some("new")) +} + +/// Тест свойства: insert -> lookup должен возвращать вставленное значение +pub fn insert_lookup_property_test() { + let test_cases = [ + #(1, "one"), + #(42, "forty-two"), + #(0, "zero"), + #(-5, "minus-five"), + ] + + list.each(test_cases, fn(test_case) { + let #(key, value) = test_case + let tree = lab2.empty() |> lab2.insert(key, value, int_compare) + lab2.lookup(tree, key, int_compare) |> should.equal(option.Some(value)) + }) +} + +/// Тест свойства: size увеличивается при вставке новых элементов +pub fn insert_size_property_test() { + let tree = lab2.empty() + lab2.size(tree) |> should.equal(0) + + let tree1 = lab2.insert(tree, 1, "one", int_compare) + lab2.size(tree1) |> should.equal(1) + + let tree2 = lab2.insert(tree1, 2, "two", int_compare) + lab2.size(tree2) |> should.equal(2) + + let tree3 = lab2.insert(tree2, 3, "three", int_compare) + lab2.size(tree3) |> should.equal(3) + + // Вставка существующего ключа не должна увеличивать размер + let tree4 = lab2.insert(tree3, 2, "two-updated", int_compare) + lab2.size(tree4) |> should.equal(3) +} + +/// Тест свойства: filter сохраняет порядок и структуру +pub fn filter_property_test() { + let original = + lab2.from_list( + [#(1, "a"), #(2, "b"), #(3, "c"), #(4, "d"), #(5, "e")], + int_compare, + ) + + // Фильтр, который пропускает все элементы + let all_filter = lab2.filter(original, fn(_, _) { True }, int_compare) + lab2.equal(original, all_filter, int_equal, string_equal) |> should.be_true + + // Фильтр, который не пропускает ничего + let none_filter = lab2.filter(original, fn(_, _) { False }, int_compare) + lab2.is_empty(none_filter) |> should.be_true + + // Частичный фильтр + let partial_filter = + lab2.filter(original, fn(key, _) { key > 3 }, int_compare) + lab2.size(partial_filter) |> should.equal(2) + lab2.contains(partial_filter, 4, int_compare) |> should.be_true + lab2.contains(partial_filter, 5, int_compare) |> should.be_true +} + +/// Тест свойства: map сохраняет структуру дерева +pub fn map_property_test() { + let original = lab2.from_list([#(1, 10), #(2, 20), #(3, 30)], int_compare) + + // Тождественное отображение + let identity_map = lab2.map(original, fn(x) { x }) + lab2.equal(original, identity_map, int_equal, int_equal) |> should.be_true + + // Отображение с изменением + let doubled = lab2.map(original, fn(x) { x * 2 }) + lab2.size(doubled) |> should.equal(lab2.size(original)) + lab2.lookup(doubled, 1, int_compare) |> should.equal(option.Some(20)) + lab2.lookup(doubled, 2, int_compare) |> should.equal(option.Some(40)) + lab2.lookup(doubled, 3, int_compare) |> should.equal(option.Some(60)) +} + +/// Тест свойства: fold_left и fold_right дают корректные результаты +pub fn fold_property_test() { + let tree = + lab2.from_list([#(1, 1), #(2, 2), #(3, 3), #(4, 4), #(5, 5)], int_compare) + + // Сумма всех значений + let sum_left = lab2.fold_left(tree, 0, fn(acc, _, value) { acc + value }) + let sum_right = lab2.fold_right(tree, 0, fn(_, value, acc) { acc + value }) + + sum_left |> should.equal(15) + sum_right |> should.equal(15) + + // Количество элементов + let count_left = lab2.fold_left(tree, 0, fn(acc, _, _) { acc + 1 }) + let count_right = lab2.fold_right(tree, 0, fn(_, _, acc) { acc + 1 }) + + count_left |> should.equal(5) + count_right |> should.equal(5) +} + +/// Тест свойства: to_list -> from_list должно давать эквивалентное дерево +pub fn list_roundtrip_property_test() { + let original_list = [#(3, "c"), #(1, "a"), #(4, "d"), #(2, "b")] + let tree = lab2.from_list(original_list, int_compare) + let result_list = lab2.to_list(tree) + let restored_tree = lab2.from_list(result_list, int_compare) + + lab2.size(tree) |> should.equal(lab2.size(restored_tree)) + lab2.equal(tree, restored_tree, int_equal, string_equal) |> should.be_true + + // Проверяем, что все элементы присутствуют + list.each(original_list, fn(item) { + let #(key, value) = item + lab2.lookup(restored_tree, key, int_compare) + |> should.equal(option.Some(value)) + }) +} + +/// Тест инвариантов моноида (коммутативность для деревьев с разными ключами) +pub fn monoid_commutativity_test() { + let tree1 = lab2.from_list([#(1, "a"), #(2, "b")], int_compare) + let tree2 = lab2.from_list([#(3, "c"), #(4, "d")], int_compare) + + let concat1 = lab2.concat(tree1, tree2, int_compare) + let concat2 = lab2.concat(tree2, tree1, int_compare) + + // Для деревьев с разными ключами конкатенация должна быть коммутативной + lab2.size(concat1) |> should.equal(4) + lab2.size(concat2) |> should.equal(4) + + // Оба дерева должны содержать все элементы + [1, 2, 3, 4] + |> list.each(fn(key) { + lab2.contains(concat1, key, int_compare) |> should.be_true + lab2.contains(concat2, key, int_compare) |> should.be_true + }) +}