Одним из огромных преимуществ лени является возможность писать неизменяемые структуры данных с разумными амортизированными границами. Простой пример - неизменный стек (с использованием F #):
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
let rec append x y =
match x with
| EmptyStack -> y
| StackNode(hd, tl) -> StackNode(hd, append tl y)
Код разумный, но добавление двух стеков x и y занимает O (длину x) времени в лучшем, худшем и среднем случаях. Добавление двух стеков является монолитной операцией, она затрагивает все узлы в стеке x.
Мы можем переписать структуру данных как ленивый стек:
type 'a lazyStack =
| StackNode of Lazy<'a * 'a lazyStack>
| EmptyStack
let rec append x y =
match x with
| StackNode(item) -> Node(lazy(let hd, tl = item.Force(); hd, append tl y))
| Empty -> y
lazy
работает, приостанавливая оценку кода в его конструкторе. После оценки с использованием .Force()
возвращаемое значение кэшируется и повторно используется в каждом последующем .Force()
.
В ленивой версии добавления - это операция O (1): она возвращает 1 узел и приостанавливает реальное восстановление списка. Когда вы получите заголовок этого списка, он оценит содержимое узла, заставит его вернуть заголовок и создаст одну приостановку с оставшимися элементами, поэтому взятие заголовка списка является операцией O (1).
Итак, наш ленивый список находится в постоянном состоянии восстановления, вы не оплачиваете стоимость восстановления этого списка, пока не пройдете все его элементы. Используя лень, этот список поддерживает O (1) согласование и добавление. Интересно, что, поскольку мы не оцениваем узлы до тех пор, пока к ним не будет получен доступ, вполне возможно построить список с потенциально бесконечными элементами.
Приведенная выше структура данных не требует пересчета узлов при каждом обходе, поэтому они заметно отличаются от ванильных IEnumerables в .NET.