Почему полезна концепция ленивых вычислений?

30

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

Как можно использовать эту парадигму для создания предсказуемого программного обеспечения, которое работает как задумано, когда у нас нет гарантии, когда и где выражение будет оценено?

akashchandrakar
источник
10
В большинстве случаев это просто не имеет значения. Для всех остальных вы можете просто соблюдать строгость.
Cat Plus Plus
22
Смысл чисто функциональных языков, таких как haskell, в том, что вам не нужно беспокоиться о запуске кода, поскольку он не имеет побочных эффектов.
битовая
21
Вам нужно перестать думать о «выполнении кода» и начать думать о «вычислении результатов», потому что это то, что вам действительно нужно в самых интересных задачах. Конечно, программы, как правило, также должны каким-то образом взаимодействовать со средой, но это часто можно уменьшить до небольшой части кода. В остальном вы можете работать чисто функционально , а лень может сделать рассуждение намного проще.
оставлено около
6
Вопрос в заголовке («Зачем использовать ленивую оценку?») Сильно отличается от вопроса в теле («Как вы используете ленивую оценку?»). Для первого см. Мой ответ на этот связанный вопрос .
Даниэль Вагнер
1
Пример, когда лень полезна: в Haskell head . sortесть O(n)сложность из-за лени (нет O(n log n)). См. Ленивая Оценка и Сложность Времени .
Петр Пудлак

Ответы:

62

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

Классический аргумент изложен в широко цитируемой статье Джона Хьюза «Почему функциональное программирование имеет значение» (ссылка в формате PDF) . Ключевым примером в этой статье (раздел 5) является игра в крестики-нолики с использованием алгоритма поиска альфа-бета. Ключевой момент (стр. 9):

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

Программа Tic-Tac-Toe может быть написана как функция, которая генерирует все игровое дерево, начиная с заданной позиции, и отдельная функция, которая его использует. Во время выполнения это по сути не генерирует целое дерево игры, а только те его части, которые действительно нужны потребителю. Мы можем изменить порядок и комбинацию, в которой производятся альтернативы, путем изменения потребителя; нет необходимости менять генератор вообще.

На нетерпеливом языке вы не можете написать это таким образом, потому что вы, вероятно, потратили бы слишком много времени и памяти на создание дерева. Таким образом, вы в конечном итоге либо:

  1. Объединение производства и потребления в одну функцию;
  2. Написание производителя, который работает оптимально только для определенных потребителей;
  3. Реализация собственной версии лени.
sacundim
источник
Пожалуйста, больше информации или пример. Это звучит интригующе.
Алекс Най
1
@AlexNye: У бумаги Джона Хьюза есть больше информации. Несмотря на то, что это академическая статья - и, следовательно, без сомнения пугающая - на самом деле она очень доступна и читаема. Если бы не его длина, это, вероятно, подошло бы в качестве ответа здесь!
Тихон Джелвис
Возможно, чтобы понять этот ответ, нужно прочитать статью Хьюза ... не сделав этого, я все еще не понимаю, как и почему лень и модульность связаны между собой.
Stakx
@stakx Без лучшего описания они, кажется, не связаны, кроме как случайно. Преимущество лени в этом примере состоит в том, что ленивый генератор способен генерировать все возможные состояния игры, но при этом не тратит впустую время / память, потому что будут потребляться только те, которые происходят. Генератор может быть отделен от потребителя, не будучи ленивым генератором, и возможно (хотя и более трудно) быть ленивым, не будучи отделенным от потребителя.
Изката
@Iztaka: «Генератор может быть отделен от потребителя, не будучи ленивым генератором, и возможно (хотя и более трудно) быть ленивым, не будучи отделенным от потребителя». Обратите внимание, что когда вы идете по этому пути, вы можете получить ** чрезмерно специализированный генератор ** - генератор был написан для оптимизации одного потребителя, а при повторном использовании для других он неоптимален. Типичным примером являются объектно-реляционные средства отображения, которые извлекают и строят целый граф объектов только потому, что вам нужна одна строка из корневого объекта. Лень избегает многих таких случаев (но не всех).
Сакундим
32

Как можно использовать эту парадигму для создания предсказуемого программного обеспечения, которое работает как задумано, когда у нас нет гарантии, когда и где выражение будет оценено?

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

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

sepp2k
источник
15
Вот почему только языки с «принудительной чистотой», такие как Haskell, поддерживают лень везде по умолчанию. Языки «поощряемой чистоты», такие как Scala, должны, чтобы программист явно сказал, где им нужна лень, и программист должен убедиться, что лень не вызовет проблем. Язык, который по умолчанию ленивый и не имеет побочных эффектов, будет действительно трудно программировать.
Бен
1
несомненно, монады кроме IO также могут вызывать побочные эффекты
jk.
1
@jk Только IO может вызвать внешние побочные эффекты.
dave4420
@ dave4420 да, но это не то, что говорит этот ответ
JK.
1
@jk В Хаскеле на самом деле нет. Ни одна монада, кроме IO (или построенных на IO), не имеет побочных эффектов. И это только потому, что компилятор по-разному относится к IO. Он думает о IO как о «неизменном выключении». Монады - это просто (умный) способ обеспечить определенный порядок выполнения (поэтому ваш файл будет удален только после того, как пользователь введет «да»).
Скарфридж
22

Если вы знакомы с базами данных, очень частый способ обработки данных:

  • Задайте вопрос как select * from foobar
  • Пока есть больше данных, выполните: получите следующий ряд результатов и обработайте его

Вы не контролируете, как генерируется результат и каким образом (индексы? Полное сканирование таблицы?) Или когда (должны ли все данные генерироваться сразу или постепенно при запросе?). Все, что вы знаете: если есть больше данных, вы получите их, когда попросите.

Ленивая оценка довольно близка к тому же. Скажем, у вас есть бесконечный список, определенный как т.е. последовательность Фибоначчи - если вам нужно пять чисел, вы получите пять чисел; если вам нужно 1000, вы получите 1000. Хитрость заключается в том, что среда выполнения знает, что и где и когда предоставлять. Это очень, очень удобно.

(Java-программисты могут эмулировать это поведение с помощью итераторов - другие языки могут иметь что-то похожее)

samthebrand
источник
Хорошая точка зрения. Например Collection2.filter()(как и другие методы этого класса) в значительной степени реализована ленивая оценка: результат «выглядит» как обычный Collection, но порядок выполнения может быть неинтуитивным (или, по крайней мере, неочевидным). Кроме того, yieldв Python есть (и похожая функция в C #, имя которой я не помню), которая даже ближе к поддержке отложенных вычислений, чем обычный Iterator.
Йоахим Зауэр
@JoachimSauer в C # его возвращаемая доходность, или, конечно, вы можете использовать предкомпилированные операторы linq, около половины из которых являются ленивыми
jk.
+1: для упоминания итераторов в императивном / объектно-ориентированном языке. Я использовал аналогичное решение для реализации потоков и функций потоков в Java. Используя итераторы, я мог бы иметь функции вроде take (n), dropWhile () для входного потока неизвестной длины.
Джорджио
13

Попробуйте запросить в вашей базе данных список первых 2000 пользователей, чьи имена начинаются с «Ab» и старше 20 лет. Также они должны быть мужчинами.

Вот небольшая диаграмма.

You                                            Program Processor
------------------------------------------------------------------------------
Get the first 2000 users ---------->---------- OK!
                         --------------------- So I'll go get those records...
WAIT! Also, they have to ---------->---------- Gotcha!
start with "Ab"
                         --------------------- NOW I'll get them...
WAIT! Make sure they're  ---------->---------- Good idea Boss!
over 20!
                         --------------------- Let's go then...
And one more thing! Make ---------->---------- Anything else? Ugh!
sure they're male!

No that is all. :(       ---------->---------- FINE! Getting records!

                         --------------------- Here you go. 
Thanks Postgres, you're  ---------->----------  ...
my only friend.

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

В противоположность получению первых 2000 пользователей, возвращая их, фильтруя их по «Ab», возвращая их, фильтруя их более 20, возвращая их, и фильтруя для мужчин и, наконец, возвращая их.

Ленивая загрузка в двух словах.

sergserg
источник
1
это действительно паршивое объяснение ИМХО. К сожалению, у меня недостаточно представителей на этом сайте SE, чтобы проголосовать за него. Истинная точка ленивой оценки в том, что ни один из этих результатов на самом деле не создается, пока что-то еще не будет готово их использовать.
Альнитак
Мой опубликованный ответ говорит то же самое, что и ваш комментарий.
sergserg
Это очень вежливый программный процессор.
Джулиан
9

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

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

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

Калеб
источник
5

Есть несколько аргументов для ленивой оценки, я думаю, это убедительно

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

    take 10 . filter (<1) . map (1/)
    

    но это просто неверно в строгом языке, так как если вы дадите его, [2,3,4,5,6,7,8,9,10,11,12,0]вы будете делить на ноль. Посмотрите ответ sacundim, почему это здорово на практике

  2. Больше вещей работает Строго (каламбур) больше программ завершается не строгой оценкой, чем строгой оценкой. Если ваша программа завершается с «энергичной» оценочной стратегией, она заканчивается с «ленивой», но обратное неверно. В качестве конкретных примеров этого явления вы получаете такие вещи, как бесконечные структуры данных (которые на самом деле просто классные). Другие программы работают на ленивых языках.

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

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

Филипп JF
источник
2

Предположим, у вас есть много дорогих вычислений, но вы не знаете, какие из них действительно понадобятся или в каком порядке. Вы можете добавить сложный протокол mother-may-i, чтобы заставить потребителя выяснить, что доступно, и инициировать вычисления, которые еще не сделаны. Или вы можете просто предоставить интерфейс, который действует так, как будто все расчеты выполнены.

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

ddyer
источник
1

с ленивой оценкой вы не теряете контроль над выполнением кода, это все еще абсолютно детерминировано. Хотя с этим трудно привыкнуть.

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

permeakra
источник