lab 2 complete v1

This commit is contained in:
2025-12-06 13:44:45 +03:00
parent 9023a42c46
commit 080bb8d422
4 changed files with 225 additions and 325 deletions
+97 -85
View File
@@ -4,7 +4,7 @@
**Факультет программной инженерии и компьютерной техники**
**Студент:** Владимиров Владислав Александрович
**Группа:** P3222
**Группа:** P3322
**Тема:** Реализация Red-Black Tree с ленивыми вычислениями
**Лабораторная работа №2**
@@ -15,7 +15,28 @@
## Требования к разработанному ПО
### Цель работы
Освоиться с построением пользовательских типов данных, полиморфизмом, рекурсивными алгоритмами и средствами тестирования (unit testing, property-based testing), а также разделением интерфейса и особенностей реализации.
Цель: освоиться с построением пользовательских типов данных, полиморфизмом, рекурсивными алгоритмами и средствами тестирования (unit testing, property-based testing), а также разделением интерфейса и особенностей реализации.
В рамках лабораторной работы вам предлагается реализовать одну из предложенных классических структур данных (список, дерево, бинарное дерево, hashmap, граф...).
Требования:
1. Функции:
- добавление и удаление элементов;
- фильтрация;
- отображение (map);
- свертки (левая и правая);
- структура должна быть [моноидом](https://ru.m.wikipedia.org/wiki/Моноид).
2. Структуры данных должны быть неизменяемыми.
3. Библиотека должна быть протестирована в рамках unit testing.
4. Библиотека должна быть протестирована в рамках property-based тестирования (как минимум 3 свойства, включая свойства моноида).
5. Структура должна быть полиморфной.
6. Требуется использовать идиоматичный для технологии стиль программирования. Примечание: некоторые языки позволяют получить большую часть API через реализацию небольшого интерфейса. Так как лабораторная работа про ФП, а не про экосистему языка -- необходимо реализовать их вручную и по возможности -- обеспечить совместимость.
7. Обратите внимание:
- API должно быть реализовано для заданного интерфейса и оно не должно "протекать". На уровне тестов -- в первую очередь нужно протестировать именно API (dict, set, bag).
- Должна быть эффективная реализация функции сравнения (не наивное приведение к спискам, их сортировка с последующим сравнением), реализованная на уровне API, а не внутреннего представления.
### Вариант задания
**Red-Black Tree Lazy** - красно-чёрное дерево на ленивых вычислениях.
@@ -55,19 +76,19 @@
### Основные типы данных
```gleam
/// Цвета узлов красно-черного дерева
// Цвета узлов красно-черного дерева
pub type Color {
Red
Black
}
/// Ленивое значение для отложенных вычислений
// Ленивое значение для отложенных вычислений
pub type Lazy(a) {
Thunk(fn() -> a) // Отложенное вычисление
Value(a) // Уже вычисленное значение
}
/// Красно-черное дерево с ленивыми вычислениями
// Красно-черное дерево с ленивыми вычислениями
pub type RBTree(k, v) {
Empty // Пустое дерево
Node(
@@ -83,7 +104,7 @@ pub type RBTree(k, v) {
### Управление ленивыми вычислениями
```gleam
/// Форсирует вычисление ленивого значения
// Форсирует вычисление ленивого значения
fn force(lazy: Lazy(a)) -> a {
case lazy {
Value(val) -> val
@@ -91,7 +112,7 @@ fn force(lazy: Lazy(a)) -> a {
}
}
/// Создаёт ленивое значение из функции
// Создаёт ленивое значение из функции
fn delay(f: fn() -> a) -> Lazy(a) {
Thunk(f)
}
@@ -171,6 +192,40 @@ n balance(
### Основные операции
**Главное - сравнение**
```gleam
// Вспомогательная функция для структурного сравнения деревьев
// Использует замыкание при первом несовпадении
fn 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(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)
}
}
}
```
**Вставка элемента:**
```gleam
pub fn insert(tree: RBTree(k, v), key: k, value: v,
@@ -228,12 +283,12 @@ pub fn fold_left(tree: RBTree(k, v), acc: a, f: fn(a, k, v) -> a) -> a {
### Операции моноида
```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) {
@@ -247,14 +302,19 @@ pub fn concat(tree1: RBTree(k, v), tree2: RBTree(k, v),
## Тесты и метрики
### 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()` - конвертация в список и обратно
- `empty_tree_test()` - создание пустого дерева и проверка `is_empty()`, `size()`
- `single_insert_test()` - вставка одного элемента, проверка `lookup()` и размера
- `multiple_insert_test()` - множественные вставки с проверкой всех элементов
- `delete_test()` - удаление элементов с проверкой корректности
- `update_existing_key_test()` - обновление существующих ключей
- `filter_test()` - фильтрация четных чисел, проверка размера и содержимого
- `map_test()` - отображение значений (умножение на 2), проверка корректности
- `fold_left_test()` - левая свёртка для суммирования значений
- `fold_right_test()` - правая свёртка для конкатенации строк
- `to_from_list_test()` - конвертация дерево→список→дерево с проверкой эквивалентности
- `red_black_invariant_test()` - работа с большим деревом (10 элементов), проверка балансировки
- `semantic_equal_test()` - семантическое равенство (независимо от структуры)
- `structure_equal_test()` - структурное равенство без учёта цветов узлов
### Property-based тесты
@@ -279,92 +339,44 @@ pub fn concat(tree1: RBTree(k, v), tree2: RBTree(k, v),
### Тесты моноида
```gleam
/// Левая единица: mempty ∘ a = a
// Левая единица: mempty ∘ a = a
pub fn monoid_left_identity_test()
/// Правая единица: a ∘ mempty = a
// Правая единица: a ∘ mempty = a
pub fn monoid_right_identity_test()
/// Ассоциативность: (a ∘ b) ∘ c = a ∘ (b ∘ c)
// Ассоциативность: (a ∘ b) ∘ c = a ∘ (b ∘ c)
pub fn monoid_associativity_test()
/// Коммутативность (для непересекающихся множеств ключей)
// Коммутативность (для непересекающихся множеств ключей)
pub fn monoid_commutativity_test()
```
### Отчёт тестирования
```
Running lab2_test.main
.....................
21 passed, no failures
> gleam test
Compiling lab2
Compiled in 0.46s
Running lab2_test.main
.......................
23 passed, no failures
```
### Метрики производительности
| Операция | Теоретическая сложность | Реализованная сложность |
|----------|------------------------|------------------------|
| 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) |
| Операция | Сложность|
|----------|------------------------|
| lookup | O(log n) |
| insert | O(log n) |
| delete | O(log n) |
| size | O(n) |
| map | O(n) |
| filter | O(n log n) |
| fold | 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 продемонстрировала эффективность функциональных подходов к программированию. Использование неизменяемых структур данных, полиморфизма и функций высшего порядка привело к созданию гибкой, безопасной и переиспользуемой библиотеки.
Ленивые вычисления показали свою полезность для оптимизации производительности при работе с большими деревьями, где не все поддеревья могут быть использованы.
Использование ленивых вычислений повышает эффективность использования памяти при операциях со структурами данных высокой вложенности (в нашем случае) ценой cpu на вычисление значения.
Также использовал типы, а ещё писал своё сравнение без высокоуровневых абстракций.