Нетривиальная ленивая оценка

Я сейчас перевариваю приятную презентацию. Зачем изучать Haskell? Киган Макаллистер. Там он использует фрагмент

minimum = head . sort

как иллюстрация ленивой оценки Хаскелла, заявив, что minimum имеет временную сложность O (n) в Haskell. Однако, я думаю, что этот пример носит академический характер. Поэтому я прошу более практичный пример, где тривиально очевидно, что большая часть промежуточных вычислений выбрасывается.

Ответ 1

  • Вы когда-нибудь писали ИИ? Разве не раздражает, что вам нужно обрезать информацию об обрезке (например, максимальную глубину, минимальную стоимость соседней ветки или другую такую ​​информацию) через функцию обхода дерева? Это означает, что вам нужно писать новый обход дерева каждый раз, когда вы хотите улучшить свой ИИ. Это глупо. С ленивой оценкой это уже не проблема: напишите свою функцию обхода дерева один раз, чтобы создать огромное (возможно, даже бесконечное!) Игровое дерево, и пусть ваш потребитель решает, сколько из него потреблять.

  • Написание графического интерфейса, который отображает много информации? Хочешь, чтобы он работал быстро? На других языках вам может потребоваться написать код, который отображает только видимые сцены. В Haskell вы можете написать код, который отображает всю сцену, а затем выбрать, какие пиксели будут наблюдаться. Аналогично, создавая сложную сцену? Почему бы не вычислить бесконечную последовательность сцен на разных уровнях детализации и выбрать наиболее подходящую из них при запуске программы?

  • Вы пишете дорогостоящую функцию и решаете записать ее для скорости. В других языках это требует создания структуры данных, которая отслеживает, какие входы для функции, которой вы знаете, ответ, и обновляете структуру по мере появления новых входов. Не забудьте сделать его потокобезопасным - если нам действительно нужна скорость, нам понадобится parallelism! В Haskell вы создаете бесконечную структуру данных с записью для каждого возможного ввода и оцениваете части структуры данных, которые соответствуют интересующим вас входам. Безопасность резьбы поставляется бесплатно с чистотой.

  • Вот один, который, возможно, немного прозаичен, чем предыдущие. Вы когда-нибудь находили время, когда && и || были не единственными вещами, которые вы хотели бы закоротить? Уверен! Например, мне нравится функция <|> для объединения значений Maybe: она берет первый из своих аргументов, который фактически имеет значение. Итак Just 3 <|> Nothing = Just 3; Nothing <|> Just 7 = Just 7; и Nothing <|> Nothing = Nothing. Более того, это короткое замыкание: если окажется, что его первый аргумент равен Just, он не будет беспокоиться о выполнении вычислений, необходимых для выяснения того, каков его второй аргумент.

    И <|> не встроен в язык; он подхватил библиотеку. То есть: лень позволяет писать совершенно новые формы короткого замыкания. (Действительно, в Haskell даже короткозамкнутое поведение (&&) и (||) не является встроенной магией компилятора: они естественным образом возникают из семантики языка и их определений в стандартных библиотеках.)

В общем, общая тема здесь заключается в том, что вы можете отделить производство ценностей от определения того, какие значения интересны для просмотра. Это делает вещи более сложными, потому что выбор того, что интересно смотреть, не должен быть известен производителю.

Ответ 2

Вот известный пример, который я вчера опубликовал в другой теме. Числа Хэмминга - это числа, которые не имеют каких-либо простых факторов, больших 5. I.e. они имеют вид 2 ^ я * 3 ^ j * 5 ^ k. Первые 20 из них:

[1,2,3,4,5,6,8,9,10,12,15,16,18,20,24,25,27,30,32,36]

500 000-й:

1962938367679548095642112423564462631020433036610484123229980468750

Программа, которая напечатала 500000-й (после краткого момента вычисления):

merge [email protected](x:xs) [email protected](y:ys) =
  case (x`compare`y) of
    LT -> x:merge xs yys
    EQ -> x:merge xs ys
    GT -> y:merge xxs ys

hamming = 1 : m 2 `merge` m 3 `merge` m 5
  where
    m k = map (k *) hamming

main = print (hamming !! 499999)

Вычисление этого числа с разумной скоростью на ленивом языке требует немного большего количества кода и царапин на голове. Здесь много примеров

Ответ 3

Рассмотрим создание и потребление первых элементов n бесконечной последовательности. Без ленивой оценки наивное кодирование будет выполняться вечно на этапе генерации и никогда ничего не потреблять. С ленивой оценкой генерируется столько элементов, сколько код пытается использовать.