Чем полезна ленивая оценка?

119

Я давно задавался вопросом, чем полезна ленивая оценка. Мне еще предстоит, чтобы кто-нибудь объяснил мне разумным образом; в основном все сводится к «поверь мне».

Примечание: я не имею в виду мемоизацию.

Джоэл МакКракен
источник

Ответы:

96

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

Он также позволяет создавать такие интересные вещи, как бесконечные списки. У меня не может быть бесконечного списка на таком языке, как C, но в Haskell это не проблема. Бесконечные списки довольно часто используются в определенных областях математики, поэтому может быть полезно иметь возможность манипулировать ими.

mipadi
источник
6
Python лениво вычислял бесконечные списки с помощью итераторов
Марк Сидаде
4
На самом деле вы можете эмулировать бесконечный список в Python, используя генераторы и выражения генераторов (которые работают аналогично пониманию списка): python.org/doc/2.5.2/ref/genexpr.html
Джон Монтгомери,
24
Генераторы упрощают создание ленивых списков в Python, но другие методы ленивых вычислений и структуры данных заметно менее элегантны.
Питер Бернс,
3
Боюсь, я не согласен с таким ответом. Раньше я думал, что лень связана с эффективностью, но, использовав много Haskell, а затем переключившись на Scala и сравнивая опыт, я должен был сказать, что лень имеет значение часто, но редко из-за эффективности. Я думаю, что Эдвард Кметт раскрывает настоящие причины.
Owen
3
Я также не согласен, хотя в C нет явного понятия бесконечного списка из-за строгой оценки, вы можете легко проделать тот же трюк на любом другом языке (и, действительно, в большинстве фактических реализаций ленивого языка), используя thunks и передавая функцию указатели для работы с конечным префиксом бесконечной структуры, создаваемой аналогичными выражениями.
Кристофер Мицински
71

Полезным примером ленивого вычисления является использование quickSort:

quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)

Если теперь мы хотим найти минимум списка, мы можем определить

minimum ls = head (quickSort ls)

Которая сначала сортирует список, а затем берет первый элемент списка. Однако из-за ленивой оценки вычисляется только голова. Например, если мы возьмем минимум списка, [2, 1, 3,]quickSort сначала отфильтрует все элементы, которые меньше двух. Затем он выполняет quickSort (возвращая список синглтонов [1]), чего уже достаточно. Из-за ленивого вычисления остальное никогда не сортируется, что позволяет сэкономить много вычислительного времени.

Это, конечно, очень простой пример, но лень работает таким же образом для очень больших программ.

Однако у всего этого есть обратная сторона: становится труднее предсказать скорость выполнения и использование памяти вашей программой. Это не означает, что ленивые программы работают медленнее или занимают больше памяти, но это полезно знать.

Крис Эйдхоф
источник
19
В более общем смысле, take k $ quicksort listтребуется только время O (n + k log k), где n = length list. При сортировке неленивым сравнением это всегда будет занимать время O (n log n).
ephemient
@ephemient, разве вы не имеете в виду O (nk log k)?
MaiaVictor
1
@Viclib Нет, я имел в виду то, что сказал.
ephemient
@ephemient, тогда я думаю, что не понимаю, к сожалению
MaiaVictor
2
@Viclib Алгоритм выбора для поиска верхних k элементов из n равен O (n + k log k). Когда вы реализуете быструю сортировку на ленивом языке и оцениваете ее только для определения первых k элементов (останавливая оценку после), он выполняет те же сравнения, что и алгоритм неленивого выбора.
ephemient
70

Я считаю, что ленивое вычисление полезно для многих вещей.

Во-первых, все существующие ленивые языки чисты, потому что на ленивом языке очень сложно рассуждать о побочных эффектах.

Чистые языки позволяют вам рассуждать об определениях функций, используя уравнительные рассуждения.

foo x = x + 3

К сожалению, в неленивом режиме больше операторов не удается вернуть, чем в ленивом, поэтому это менее полезно в таких языках, как ML. Но ленивым языком можно смело рассуждать о равенстве.

Во-вторых, многие вещи вроде «ограничения значений» в ML не нужны в ленивых языках вроде Haskell. Это приводит к значительному упрощению синтаксиса. ML-подобные языки должны использовать такие ключевые слова, как var или fun. В Haskell все это сводится к одному понятию.

В-третьих, лень позволяет писать очень функциональный код, который можно понимать по частям. В Haskell обычно пишут тело функции, например:

foo x y = if condition1
          then some (complicated set of combinators) (involving bigscaryexpression)
          else if condition2
          then bigscaryexpression
          else Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

Это позволяет вам работать «сверху вниз», понимая основную часть функции. ML-подобные языки заставляют вас использовать letстрого оцененный. Следовательно, вы не осмеливаетесь «поднять» предложение let до основного тела функции, потому что, если оно дорого (или имеет побочные эффекты), вы не хотите, чтобы оно всегда оценивалось. Haskell может явно «отодвинуть» детали в предложение where, поскольку знает, что содержимое этого предложения будет оцениваться только по мере необходимости.

На практике мы, как правило, используем охранники и обрушиваем их, чтобы:

foo x y 
  | condition1 = some (complicated set of combinators) (involving bigscaryexpression)
  | condition2 = bigscaryexpression
  | otherwise  = Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

В-четвертых, иногда лень предлагает гораздо более элегантное выражение определенных алгоритмов. Ленивая «быстрая сортировка» в Haskell является однострочным и имеет то преимущество, что если вы смотрите только на несколько первых элементов, вы платите только те затраты, которые пропорциональны стоимости выбора только этих элементов. Ничто не мешает вам делать это строго, но вам, вероятно, придется каждый раз перекодировать алгоритм для достижения той же асимптотической производительности.

В-пятых, лень позволяет вам определять в языке новые управляющие структуры. Вы не можете написать новую конструкцию типа «если ... то ... еще ...» на строгом языке. Если вы попытаетесь определить такую ​​функцию, как:

if' True x y = x
if' False x y = y

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

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

Эдвард КМЕТТ
источник
5
Очень хорошо; это настоящие ответы. Раньше я думал, что речь идет об эффективности (отложении вычислений на потом), пока я не стал использовать Haskell в значительной степени и не увидел, что причина вовсе не в этом.
Owen
11
Кроме того, хотя технически неверно, что ленивый язык должен быть чистым (например, R), верно, что нечистый ленивый язык может делать очень странные вещи (например, R).
Оуэн,
4
Конечно, есть. Строго letговоря, рекурсивный - опасный зверь, в схеме R6RS он позволяет случайным символам #fпоявляться в вашем термине везде, где завязывание узла строго ведет к циклу! Никакой каламбур, но строго более рекурсивные letпривязки разумны на ленивом языке. Строгость также усугубляет тот факт, что whereнет никакого способа упорядочить относительные эффекты вообще, за исключением SCC, это конструкция уровня оператора, ее эффекты могут происходить строго в любом порядке, и даже если у вас чистый язык, вы в конечном итоге #fвопрос. Строгие whereзагадки вашего кода нелокальными проблемами.
Эдвард КМЕТТ 08
2
Не могли бы вы объяснить, как лень помогает избежать ограничения ценности? Я не мог этого понять.
Том Эллис
3
@PaulBone О чем ты говоришь? Лень во многом связана с управляющими структурами. Если вы определяете свою собственную структуру управления на строгом языке, вам придется либо использовать кучу лямбд или что-то подобное, либо это будет отстой. Потому ifFunc(True, x, y)что буду оценивать и то, xи другое, а yне просто x.
точка с запятой
28

Есть разница между вычислением нормального порядка и ленивым вычислением (как в Haskell).

square x = x * x

Вычисляя следующее выражение ...

square (square (square 2))

... с нетерпеливой оценкой:

> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256

... при нормальной оценке порядка:

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * (square (square 2))
> ((2 * 2) * (square 2)) * (square (square 2))
> (4 * (square 2)) * (square (square 2))
> (4 * (2 * 2)) * (square (square 2))
> (4 * 4) * (square (square 2))
> 16 * (square (square 2))
> ...
> 256

... с ленивой оценкой:

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * ((square 2) * (square 2))
> ((2 * 2) * (2 * 2)) * ((2 * 2) * (2 * 2))
> (4 * 4) * (4 * 4)
> 16 * 16
> 256

Это потому, что ленивое вычисление смотрит на дерево синтаксиса и выполняет преобразования дерева ...

square (square (square 2))

           ||
           \/

           *
          / \
          \ /
    square (square 2)

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
        square 2

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
           *
          / \
          \ /
           2

... тогда как обычная оценка порядка выполняет только текстовые расширения.

Вот почему мы, используя ленивое вычисление, получаем больше возможностей (оценка завершается чаще, чем другие стратегии), в то время как производительность эквивалентна активной оценке (по крайней мере, в O-нотации).

Томас Данекер
источник
25

Ленивая оценка связана с процессором так же, как сборка мусора с оперативной памятью. GC позволяет вам делать вид, что у вас неограниченный объем памяти, и, таким образом, запрашивать столько объектов в памяти, сколько вам нужно. Среда выполнения автоматически восстановит неиспользуемые объекты. LE позволяет вам притвориться, что у вас неограниченные вычислительные ресурсы - вы можете выполнять столько вычислений, сколько вам нужно. Время выполнения просто не будет выполнять ненужные (в данном случае) вычисления.

В чем практическая польза от этих «притворных» моделей? Он освобождает разработчика (до некоторой степени) от управления ресурсами и удаляет некоторый шаблонный код из ваших источников. Но более важно то, что вы можете эффективно повторно использовать свое решение в более широком наборе контекстов.

Представьте, что у вас есть список чисел S и число N. Вам нужно найти ближайшее к числу N число M из списка S. У вас может быть два контекста: одиночный N и некоторый список L из N (ei для каждого N в L вы посмотрите ближайший M в S). Если вы используете ленивую оценку, вы можете отсортировать S и применить двоичный поиск, чтобы найти ближайший M к N. Для хорошей ленивой сортировки потребуется O (размер (S)) шагов для одного N и O (ln (size (S)) * (размер (S) + размер (L))) шагов для равномерно распределенного L.Если у вас нет ленивой оценки для достижения оптимальной эффективности, вы должны реализовать алгоритм для каждого контекста.

Алексей
источник
Мне немного помогла аналогия с GC, но не могли бы вы привести пример «удаляет шаблонный код»?
Абдул
1
@Abdul, пример, знакомый любому пользователю ORM: ленивая загрузка ассоциации. Он загружает ассоциацию из БД «точно в срок» и в то же время освобождает разработчика от необходимости явно указывать, когда его загружать и как кэшировать (я имею в виду шаблон). Вот еще один пример: projectlombok.org/features/GetterLazy.html .
Алексей
25

Если верить Саймону Пейтону Джонсу, ленивое оценивание не важно само по себе, а только как «рубашка для волос», заставляющая дизайнеров сохранять язык чистым. Я сочувствую этой точке зрения.

Ричард Берд, Джон Хьюз и, в меньшей степени, Ральф Хинце способны делать удивительные вещи с ленивой оценкой. Прочтение их работ поможет вам оценить это. Хорошей отправной точкой являются великолепный решатель судоку Берда и статья Хьюза « Почему так важно функциональное программирование» .

Норман Рэмси
источник
Это не только заставляло их поддерживать язык в чистоте, но и позволяло им это делать, когда (до введения IOмонады) подпись mainбыла бы String -> Stringи вы уже могли бы правильно писать интерактивные программы.
leftaround примерно
@leftaroundabout: Что мешает строгому языку использовать все эффекты в IOмонаде?
Том Эллис
13

Рассмотрим программу игры в крестики-нолики. У него четыре функции:

  • Функция генерации ходов, которая берет текущую доску и генерирует список новых досок, к каждой из которых применен один ход.
  • Затем существует функция «дерева ходов», которая применяет функцию генерации ходов для получения всех возможных позиций доски, которые могут следовать из этого.
  • Существует минимаксная функция, которая обходит дерево (или, возможно, только его часть), чтобы найти лучший следующий ход.
  • Существует функция оценки доски, которая определяет, выиграл ли один из игроков.

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

Теперь попробуем реализовать шахматы вместо крестиков-ноликов. На «нетерпеливом» (то есть традиционном) языке это не сработает, потому что дерево перемещений не помещается в памяти. Итак, теперь функции оценки платы и генерации ходов должны быть смешаны с деревом ходов и минимаксной логикой, потому что минимаксная логика должна использоваться, чтобы решить, какие ходы генерировать. Наша красивая чистая модульная структура исчезает.

Однако в ленивом языке элементы дерева перемещений генерируются только в ответ на требования функции минимакса: нет необходимости генерировать все дерево перемещений, прежде чем мы позволим минимаксу ослабить верхний элемент. Так что наша чистая модульная структура по-прежнему работает в реальной игре.

Пол Джонсон
источник
1
[В «нетерпеливом» (то есть традиционном) языке это не сработает, потому что дерево движений не умещается в памяти] - для Tic-Tac-Toe, безусловно, будет. Можно сохранить не более 3 ** 9 = 19683 позиций. Если мы сохраним каждый из них в экстравагантных 50 байтах, это почти один мегабайт. Ничего подобного ...
Йонас Кёлькер
6
Да, это моя точка зрения. Страстные языки могут иметь чистую структуру для тривиальных игр, но должны идти на компромисс с этой структурой для чего-то реального. У ленивых языков такой проблемы нет.
Пол Джонсон,
3
Честно говоря, ленивое оценивание может привести к проблемам с собственной памятью. Люди нередко спрашивают, почему haskell выкидывает память из-за чего-то, что, в условиях нетерпеливой оценки, потребляло бы памяти O (1)
RHSeeger
@PaulJohnson Если вы оцениваете все позиции, не имеет значения, оцениваете вы их жадно или лениво. Необходимо проделать ту же работу. Если вы остановитесь посередине и оцените только половину позиций, это тоже не имеет никакого значения, потому что в обоих случаях половина работы должна быть сделана. Единственная разница между этими двумя оценками заключается в том, что алгоритм выглядит лучше, если он написан лениво.
исключение
12

Вот еще два момента, которые, как мне кажется, еще не были затронуты в ходе обсуждения.

  1. Лень - это механизм синхронизации в параллельной среде. Это легкий и легкий способ создать ссылку на какое-либо вычисление и поделиться его результатами между многими потоками. Если несколько потоков попытаются получить доступ к неоцененному значению, только один из них выполнит его, а остальные заблокируются соответственно, получив значение, как только оно станет доступным.

  2. Лень - основа амортизации структур данных в чистом виде. Это подробно описано Окасаки в « Чисто функциональных структурах данных» , но основная идея состоит в том, что ленивое вычисление является контролируемой формой мутации, критически важной для того, чтобы мы могли эффективно реализовывать определенные типы структур данных. Хотя мы часто говорим о лени, вынуждающей нас носить чистую рубашку для волос, применим и другой способ: это пара синергетических языковых особенностей.

Эдвард З. Янг
источник
10

Когда вы включаете компьютер, и Windows воздерживается от открытия каждого каталога на вашем жестком диске в проводнике Windows и воздерживается от запуска каждой отдельной программы, установленной на вашем компьютере, до тех пор, пока вы не укажете, что требуется определенный каталог или определенная программа, что это «ленивая» оценка.

«Ленивая» оценка - это выполнение операций, когда и по мере необходимости. Это полезно, когда это функция языка программирования или библиотеки, потому что обычно сложнее реализовать ленивую оценку самостоятельно, чем просто предварительно вычислить все заранее.

yfeldblum
источник
1
Некоторые могут сказать, что это действительно «ленивая казнь». На самом деле разница несущественна, за исключением достаточно чистых языков, таких как Haskell; но разница в том, что задерживаются не только вычисления, но и связанные с ними побочные эффекты (например, открытие и чтение файлов).
Оуэн,
8

Учти это:

if (conditionOne && conditionTwo) {
  doSomething();
}

Метод doSomething () будет выполняться, только если conditionOne истинно, а conditionTwo истинно. В случае, когда conditionOne ложно, зачем вам вычислять результат conditionTwo? В этом случае оценка conditionTwo будет пустой тратой времени, особенно если ваше условие является результатом некоторого процесса метода.

Это один из примеров ленивого интереса к оценке ...

Ромен Линсолас
источник
Я подумал, что это короткое замыкание, а не ленивая оценка.
Томас Оуэнс
2
Это ленивая оценка, поскольку conditionTwo вычисляется только в том случае, если это действительно необходимо (например, если conditionOne истинно).
Romain Linsolas 05
7
Я полагаю, что короткое замыкание - это вырожденный случай ленивого вычисления, но это определенно не общий способ думать об этом.
rmeador 05
19
Короткое замыкание на самом деле является частным случаем ленивого вычисления. Очевидно, что ленивое вычисление включает в себя гораздо больше, чем просто короткое замыкание. Или, что имеет короткое замыкание помимо ленивой оценки?
yfeldblum 04
2
@ Джульетта: У вас есть четкое определение слова «ленивый». Ваш пример функции, принимающей два параметра, не то же самое, что короткое замыкание оператора if. Короткое замыкание оператора if позволяет избежать ненужных вычислений. Я думаю, что лучшим сравнением с вашим примером будет оператор Visual Basic «andalso», который заставляет оценивать оба условия
8
  1. Это может повысить эффективность. Это кажется очевидным, но на самом деле не самым важным. (Обратите внимание, что лень тоже может убить эффективность - этот факт не сразу очевиден. Однако, сохраняя много временных результатов, а не вычисляя их немедленно, вы можете использовать огромное количество оперативной памяти.)

  2. Он позволяет определять конструкции управления потоком в обычном коде пользовательского уровня, а не жестко закодировать его в языке. (Например, в Java есть forциклы; в Haskell есть forфункция. В Java есть обработка исключений; в Haskell есть различные типы монад исключений. В C # есть goto; в Haskell есть монада продолжения ...)

  3. Это позволяет вам отделить алгоритм генерации данных от алгоритма для принятия решения о том, сколько данных генерировать. Вы можете написать одну функцию, которая генерирует условно бесконечный список результатов, и другую функцию, которая обрабатывает столько из этого списка, сколько сочтет нужным. Более того, у вас может быть пять функций-генераторов и пять функций-потребителей, и вы можете эффективно создавать любую комбинацию - вместо того, чтобы вручную кодировать 5 x 5 = 25 функций, которые объединяют оба действия одновременно. (!) Все мы знаем, что развязка - это хорошо.

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

MathematicalOrchid
источник
6

Одно из огромных преимуществ лени - это возможность писать неизменяемые структуры данных с разумными амортизируемыми границами. Простой пример - неизменяемый стек (с использованием 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.

Джульетта
источник
5

Этот фрагмент показывает разницу между ленивым и неленивым вычислением. Конечно, эту функцию Фибоначчи можно оптимизировать и использовать ленивую оценку вместо рекурсии, но это испортит пример.

Предположим, мы МОЖЕМ использовать для чего-то 20 первых чисел, без ленивой оценки все 20 чисел должны быть сгенерированы заранее, но при ленивом вычислении они будут генерироваться только по мере необходимости. Таким образом, при необходимости вы будете платить только расчетную цену.

Пример вывода

Не ленивое поколение: 0,023373
Ленивое поколение: 0,000009
Не ленивый вывод: 0,000921
Ленивый вывод: 0,024205
import time

def now(): return time.time()

def fibonacci(n): #Recursion for fibonacci (not-lazy)
 if n < 2:
  return n
 else:
  return fibonacci(n-1)+fibonacci(n-2)

before1 = now()
notlazy = [fibonacci(x) for x in range(20)]
after1 = now()
before2 = now()
lazy = (fibonacci(x) for x in range(20))
after2 = now()


before3 = now()
for i in notlazy:
  print i
after3 = now()

before4 = now()
for i in lazy:
  print i
after4 = now()

print "Not lazy generation: %f" % (after1-before1)
print "Lazy generation: %f" % (after2-before2)
print "Not lazy output: %f" % (after3-before3)
print "Lazy output: %f" % (after4-before4)
Винко Врсалович
источник
5

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

Чтобы увидеть это в действии, вы можете взглянуть на мою библиотеку нейронных сетей под названием instinct . Он интенсивно использует ленивую оценку для элегантности и высокой производительности. Например, я полностью избавился от традиционно императивного расчета активации. Простое ленивое выражение делает все за меня.

Это используется, например, в функции активации, а также в алгоритме обучения обратному распространению (я могу опубликовать только две ссылки, поэтому вам нужно будет самостоятельно найти learnPatфункцию в AI.Instinct.Train.Deltaмодуле). Традиционно оба требуют гораздо более сложных итерационных алгоритмов.

ertes
источник
4

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

В Haskell функция с фиксированной точкой очень проста:

fix f = f (fix f)

это расширяется до

f (f (f ....

но поскольку Haskell ленив, эта бесконечная цепочка вычислений не проблема; оценка выполняется «снаружи внутрь», и все работает прекрасно:

fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)

Важно то, что не fixленишься, а что fленишься. Как только вам уже назначили строгий fрежим, вы можете либо поднять руки вверх и сдаться, либо расширить его и засорять. (Это очень похоже на то, что Ной говорил о строгой / ленивой библиотеке , а не о языке).

А теперь представьте, что вы пишете ту же функцию на строгом Scala:

def fix[A](f: A => A): A = f(fix(f))

val fact = fix[Int=>Int] { f => n =>
    if (n == 0) 1
    else n*f(n-1)
}

Вы, конечно, получите переполнение стека. Если вы хотите, чтобы это работало, вам нужно указать fаргумент по мере необходимости:

def fix[A](f: (=>A) => A): A = f(fix(f))

def fact1(f: =>Int=>Int) = (n: Int) =>
    if (n == 0) 1
    else n*f(n-1)

val fact = fix(fact1)
Оуэн
источник
3

Я не знаю, как вы в настоящее время думаете о вещах, но я считаю полезным рассматривать ленивую оценку как проблему библиотеки, а не как функцию языка.

Я имею в виду, что в строгих языках я могу реализовать ленивую оценку, создав несколько структур данных, а в ленивых языках (по крайней мере, Haskell) я могу попросить строгости, когда я этого хочу. Следовательно, выбор языка на самом деле не делает ваши программы ленивыми или неленивыми, а просто влияет на то, какой язык вы получаете по умолчанию.

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

Ной Лавин
источник
1
Реализация ленивых вычислений на строгих языках часто является тарпитом Тьюринга.
itsbruce
2

Самым полезным применением ленивых вычислений, которое я использовал, была функция, которая вызывала серию подфункций в определенном порядке. Если одна из этих подфункций завершилась неудачно (вернула ложь), вызывающая функция должна была немедленно вернуться. Так что я мог сделать это так:

bool Function(void) {
  if (!SubFunction1())
    return false;
  if (!SubFunction2())
    return false;
  if (!SubFunction3())
    return false;

(etc)

  return true;
}

или более элегантное решение:

bool Function(void) {
  if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
    return false;
  return true;
}

Как только вы начнете его использовать, вы увидите возможности использовать его все чаще и чаще.

Марк Бернье
источник
2

Без ленивой оценки вам не разрешат написать что-то вроде этого:

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }
peeles
источник
Ну, имо, это плохая идея. Хотя этот код может быть правильным (в зависимости от того, чего вы пытаетесь достичь), его трудно читать, что всегда плохо.
Бранн,
12
Я так не думаю. Это стандартная конструкция в C и его родственниках.
Пол Джонсон,
Это пример оценки короткого замыкания, а не ленивой оценки. Или это фактически одно и то же?
RufusVS
2

Среди прочего, ленивые языки допускают многомерные бесконечные структуры данных.

Хотя схема, Python и т. Д. Допускают одномерные бесконечные структуры данных с потоками, вы можете перемещаться только по одному измерению.

Лень полезна для той же проблемы , но стоит отметить соединение сопрограмм, упомянутое в этой ссылке.

shapr
источник
2

Ленивое вычисление - это рассуждение бедняков по уравнениям (в идеале можно ожидать, что свойства кода будут выводиться из свойств задействованных типов и операций).

Пример , где она работает достаточно хорошо: sum . take 10 $ [1..10000000000]. Что мы не против того, чтобы его сократили до суммы 10 чисел вместо одного прямого и простого числового вычисления. Без ленивого вычисления, конечно, это создало бы гигантский список в памяти только для использования его первых 10 элементов. Конечно, это будет очень медленно и может вызвать ошибку нехватки памяти.

Пример , где это не так велика , как хотелось бы: sum . take 1000000 . drop 500 $ cycle [1..20]. Которая фактически суммирует 1 000 000 чисел, даже если в цикле, а не в списке; тем не менее, его следует свести к одному прямому числовому вычислению с несколькими условными выражениями и несколькими формулами. Что было бы намного лучше, чем суммировать 1 000 000 чисел. Даже если в цикле, а не в списке (т.е. после оптимизации вырубки).


Другое дело, что это позволяет кодировать в стиле хвостовой рекурсии по модулю cons , и это просто работает .

ср связанный ответ .

Уилл Несс
источник
1

Если под "ленивым вычислением" вы подразумеваете как в комбинированных логических значениях, как в

   if (ConditionA && ConditionB) ... 

тогда ответ прост: чем меньше циклов процессора потребляет программа, тем быстрее она будет работать ... и если фрагмент инструкций обработки не повлияет на результат программы, тогда в этом нет необходимости (и, следовательно, времени), чтобы выполнить их в любом случае ...

если otoh, вы имеете в виду то, что я называю "ленивыми инициализаторами", например:

class Employee
{
    private int supervisorId;
    private Employee supervisor;

    public Employee(int employeeId)
    {
        // code to call database and fetch employee record, and 
        //  populate all private data fields, EXCEPT supervisor
    }
    public Employee Supervisor
    { 
       get 
          { 
              return supervisor?? (supervisor = new Employee(supervisorId)); 
          } 
    }
}

Что ж, этот метод позволяет клиентскому коду, использующему класс, избежать необходимости вызывать базу данных для записи данных супервизора, за исключением случаев, когда клиент, использующий объект Employee, требует доступа к данным супервизора ... это ускоряет процесс создания экземпляра Employee, и все же, когда вам понадобится Supervisor, первый вызов свойства Supervisor вызовет вызов базы данных, и данные будут извлечены и доступны ...

Чарльз Бретана
источник
0

Выдержка из функций высшего порядка

Давайте найдем наибольшее число меньше 100 000, которое делится на 3829. Для этого мы просто отфильтруем набор возможностей, в которых, как мы знаем, лежит решение.

largestDivisible :: (Integral a) => a  
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0 

Сначала мы составляем список всех чисел меньше 100000 по убыванию. Затем мы фильтруем его по нашему предикату, и поскольку числа отсортированы по убыванию, наибольшее число, удовлетворяющее нашему предикату, является первым элементом отфильтрованного списка. Нам даже не нужно было использовать конечный список для нашего начального набора. Это снова лень в действии. Поскольку в конечном итоге мы используем только заголовок отфильтрованного списка, не имеет значения, является ли отфильтрованный список конечным или бесконечным. Оценка останавливается, когда найдено первое адекватное решение.

onmyway133
источник