15 KiB
Отчёт по лабораторной работе №2
Университет ИТМО
Факультет программной инженерии и компьютерной техники
Студент: Владимиров Владислав Александрович Группа: P3222
Тема: Реализация Red-Black Tree с ленивыми вычислениями
Лабораторная работа №2
Дисциплина: Функциональное программирование
Требования к разработанному ПО
Цель работы
Освоиться с построением пользовательских типов данных, полиморфизмом, рекурсивными алгоритмами и средствами тестирования (unit testing, property-based testing), а также разделением интерфейса и особенностей реализации.
Вариант задания
Red-Black Tree Lazy - красно-чёрное дерево на ленивых вычислениях.
Функциональные требования
-
Основные операции:
- Добавление элементов (
insert) - Удаление элементов (
delete) - Фильтрация (
filter) - Отображение (
map) - Свёртки левая и правая (
fold_left,fold_right)
- Добавление элементов (
-
Структурные требования:
- Структура должна быть моноидом
- Неизменяемые структуры данных
- Полиморфная реализация
- Использование ленивых вычислений
-
Требования к тестированию:
- Unit testing для всех функций
- Property-based тестирование (минимум 3 свойства)
- Тестирование свойств моноида
Технические требования
- Язык программирования: Gleam
- Сложность операций: O(log n) для insert, delete, lookup
- API не должно "протекать"
- Эффективная реализация сравнения деревьев
- Идиоматичный стиль программирования
Ключевые элементы реализации
Основные типы данных
/// Цвета узлов красно-черного дерева
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)), // Правое поддерево (ленивое)
)
}
Управление ленивыми вычислениями
/// Форсирует вычисление ленивого значения
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:
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)
}
}
Основные операции
Вставка элемента:
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) // Корень всегда чёрный
}
Поиск элемента:
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)
}
}
}
Функции высшего порядка
Отображение:
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 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 тесты
-
Свойства основных операций:
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 сохраняет структуру -
Свойства свёрток:
pub fn fold_property_test() // корректность левой/правой свёртки -
Свойства преобразований:
pub fn list_roundtrip_property_test() // to_list -> from_list эквивалентность
Тесты моноида
/// Левая единица: 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) |
Выводы
Использованные приёмы программирования
-
Ленивые вычисления (Lazy Evaluation):
- Преимущества: Экономия памяти, возможность работы с потенциально бесконечными структурами, отложенные вычисления только при необходимости
- Реализация: Тип
Lazy(a)с конструкторамиThunkиValue - Эффект: Поддеревья создаются только при обращении к ним, что снижает накладные расходы
-
Полиморфизм:
- Преимущества: Универсальность структуры данных для любых типов ключей и значений
- Реализация: Параметрические типы
RBTree(k, v) - Эффект: Возможность использования с различными типами данных без дублирования кода
-
Неизменяемые структуры данных:
- Преимущества: Отсутствие побочных эффектов, thread-safety, упрощение рассуждений о коде
- Реализация: Все операции возвращают новые версии дерева
- Эффект: Функциональная чистота и предсказуемость
-
Структурная рекурсия:
- Преимущества: Естественность выражения алгоритмов для древовидных структур
- Реализация: Паттерн-матчинг на конструкторах типа
- Эффект: Читаемый и понятный код
-
Моноид (Monoid):
- Преимущества: Математические гарантии корректности операций объединения
- Реализация: Операции
mempty()иconcat()с проверкой аксиом - Эффект: Композируемость и предсказуемость операций
-
Функции высшего порядка:
- Преимущества: Абстракция над общими паттернами обработки данных
- Реализация:
map,filter,fold_left,fold_right - Эффект: Выразительность и переиспользуемость кода
Особенности языка Gleam
- Система типов: Строгая статическая типизация с выводом типов облегчает разработку и предотвращает ошибки
- Паттерн-матчинг: Удобный и безопасный способ работы с алгебраическими типами данных
- Отсутствие null: Использование
Option(a)делает код более безопасным - Interoperability: Возможность компиляции в Erlang и JavaScript расширяет область применения
Проблемы и ограничения
- Упрощённое удаление: Реализованный алгоритм удаления не полностью поддерживает все инварианты Red-Black Tree
- Производительность: Ленивые вычисления могут добавлять накладные расходы в некоторых случаях
- Сложность балансировки: Полная реализация всех случаев балансировки требует дополнительной работы
Общая оценка
Реализация Red-Black Tree с ленивыми вычислениями на языке Gleam продемонстрировала эффективность функциональных подходов к программированию. Использование неизменяемых структур данных, полиморфизма и функций высшего порядка привело к созданию гибкой, безопасной и переиспользуемой библиотеки.
Ленивые вычисления показали свою полезность для оптимизации производительности при работе с большими деревьями, где не все поддеревья могут быть использованы.