Ленивые вычисления
Haskell ленив: он не вычисляет значение, пока оно реально не понадобится. До этого момента оно живёт как «обещание посчитать» — thunk.
Можно взять первые три элемента бесконечного списка — и программа не зависнет. Она посчитает ровно три, потому что больше не спросили.
Большинство языков энергичны (eager): аргумент вычисляется до вызова функции. Haskell — ленив: выражение вычисляется только тогда, когда его результат действительно требуется. До этого оно хранится как отложенное вычисление — thunk (по-русски иногда «отложенка»).
let x = expensive 100 -- НЕ вычисляется сразу
-- x = thunk: «обещание посчитать expensive 100»
print x -- ВОТ здесь thunk вынуждается
-- и считается ровно один раз
Бесконечные структуры
Самое яркое следствие лени — бесконечные списки. Они описывают «как продолжать», а считается лишь нужный кусок:
nats = [1 ..] -- 1,2,3,... бесконечно
firstFive = take 5 nats -- [1,2,3,4,5] — посчитано только 5
ones = 1 : ones -- бесконечный список единиц
take 3 ones -- [1,1,1]
Поскольку take 5 запрашивает только пять элементов, остальной бесконечный список так и остаётся неподсчитанным. Программа не зацикливается.
Считается только нужное
Лень означает, что неиспользованные части выражения вообще не вычисляются:
pick :: Bool -> a -> a -> a
pick True a _ = a -- второй аргумент не тронут
pick False _ b = b -- первый аргумент не тронут
-- pick True 5 (div 1 0) -> 5, а не ошибка деления!
-- ветка с делением на ноль просто не вычисляется
В Python вычисления энергичные, но ленивость отлично моделируется генераторами: значения производятся по запросу, а не все сразу.
# Та же идея на Python: ленивость через генераторы
from itertools import count, islice
# count() — бесконечная последовательность 1,2,3,...
nats = count(1)
first_five = list(islice(nats, 5)) # считаем только 5
print(first_five) # [1, 2, 3, 4, 5]
# генератор бесконечен, но считается по требованию
def ones():
while True:
yield 1
print(list(islice(ones(), 3))) # [1, 1, 1]
Платить только за нужное
Ленивость — пожалуй, самая необычная черта Haskell для тех, кто пришёл из энергичных языков, и одновременно одна из самых мощных. Она позволяет описывать потенциально бесконечные структуры — натуральный ряд, поток простых чисел, повторяющийся узор — а вычислять ровно тот кусок, который реально запросили. Это даёт красивую композицию «генерируй бесконечно, бери сколько надо»: определение и потребление данных разъезжаются, и каждое можно писать независимо. Но у медали есть оборотная сторона: на больших агрегациях ленивые отложенные вычисления способны незаметно накапливаться и съедать память. Поэтому для числовых свёрток берут строгие версии вроде foldl', которые сразу считают промежуточный результат, а не копят цепочку обещаний. Понимание, когда лень помогает, а когда стоит подтолкнуть вычисление, — признак зрелого хаскелиста.
Как это мыслить
Представляйте, что Haskell «откладывает работу до последнего». Значение — это не «уже посчитанное число», а «рецепт, который посчитают, когда спросят». Это позволяет описывать потенциально бесконечные данные и платить только за ту часть, что реально используется.
Стоит добавить, что ленивость влияет и на то, как проектируют функции в Haskell. Поскольку аргумент не вычисляется, пока он не понадобится, можно безопасно передавать в функцию выражения, которые в некоторых ветках вообще не будут затронуты, — и не платить за их вычисление. Это поощряет модульность: вы спокойно разделяете «генерацию данных» и «их потребление», зная, что произведётся ровно столько, сколько потребит потребитель. Классический приём — описать бесконечный поток возможных решений и лениво брать из него первое подходящее. Энергичный язык такого не позволит: он попытался бы вычислить весь поток заранее и зависнет. Ленивость же превращает «бесконечность» в обычный, удобный инструмент, за который платишь лишь по факту использования.
Частые ошибки
- Накопление thunk'ов. При свёртке больших данных ленивые «отложенки» могут копиться и съедать память; помогает строгая свёртка
foldl'. - Ждать побочных эффектов «по порядку». В чистом коде порядок вычислений определяет надобность, а не текст программы.
- Печатать бесконечный список целиком.
print [1..]не остановится — обрезайте черезtake.
Best practices
- Пользуйтесь бесконечными списками и
take— это идиоматично и элегантно. - Для больших агрегирующих свёрток предпочитайте строгие версии (
foldl',sumи т. п.). - Помните: лень даёт композицию «генерируй бесконечно — бери сколько надо» почти бесплатно.
Итог. Ленивость означает «вычисляй по требованию»: выражения хранятся как thunks и считаются лишь при необходимости. Это даёт бесконечные структуры и экономию вычислений, но требует осторожности с накоплением отложенок на больших данных.