Почему ленивая оценка не используется везде?

32

Я только что узнал, как работает ленивая оценка, и мне было интересно: почему ленивая оценка не применяется в каждом производимом в настоящее время программном обеспечении? Почему по-прежнему использовать нетерпеливые оценки?

Джон Смит
источник
2
Вот пример того, что может произойти, если вы смешаете изменчивое состояние и ленивую оценку. alicebobandmallory.com/articles/2011/01/01/…
Йонас Эльфстрем
2
@ JonasElfström: Пожалуйста, не путайте изменяемое состояние с одной из его возможных реализаций. Изменяемое состояние может быть реализовано с использованием бесконечного ленивого потока значений. Тогда у вас нет проблемы изменяемых переменных.
Джорджио
В императивных языках программирования «ленивая оценка» требует сознательных усилий со стороны программиста. Общее программирование на императивных языках сделало это простым, но оно никогда не будет прозрачным. Ответ на другую сторону вопроса поднимает еще один вопрос: «Почему функциональные языки программирования не используются повсеместно?», И текущий ответ - просто «нет» с точки зрения текущих дел.
Rwong
2
Функциональные языки программирования не используются повсеместно по той же причине, по которой мы не используем молотки на винтах, не все проблемы могут быть легко выражены в виде функционального ввода -> вывода, например, GUI лучше подходит для выражения в императивной форме. ,
ALXGTV
Кроме того, существует два класса функциональных языков программирования (или, по крайней мере, оба утверждают, что они функциональны), императивные функциональные языки, например, Clojure, Scala, и декларативные, например, Haskell, OCaml.
ALXGTV

Ответы:

38

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

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

В-третьих, ленивая оценка подразумевает потерю контроля. Что если я лениво оценил чтение файла с диска? Или получать время? Это не приемлемо.

Стремительная оценка может быть более эффективной и более управляемой, и тривиально преобразуется в ленивую оценку. Зачем вам ленивая оценка?

DeadMG
источник
10
Ленивое чтение файла с диска на самом деле очень удобно - для большинства моих простых программ и сценариев Haskell readFile- именно то , что мне нужно. Кроме того, переход от ленивых к нетерпеливым оценкам так же тривиален.
Тихон Джелвис
3
Согласен с вами всем, кроме последнего абзаца. Ленивая оценка более эффективна, когда есть цепная операция, и она может лучше контролировать, когда вам действительно нужны данные
texasbruce
4
Законы о функторах хотели бы поговорить с вами о «потере контроля». Если вы пишете чистые функции, которые работают с неизменяемыми типами данных, ленивая оценка - это находка. Такие языки, как haskell, основаны на концепции лени. В некоторых языках это громоздко, особенно когда смешано с «небезопасным» кодом, но вы говорите так, будто по умолчанию лень опасна или вредна. Это только «опасно» в опасном коде.
Сара
1
@DeadMG Нет, если вам небезразлично, заканчивается ли ваш код или нет ... Что это head [1 ..]дает вам на жадно оцененном чистом языке, потому что в Haskell это дает 1?
точка с запятой
1
Для многих языков реализация отложенной оценки, по крайней мере, создаст сложность. Иногда такая сложность необходима, и ленивая оценка повышает общую эффективность, особенно если то, что оценивается, требуется только условно. Однако, если все сделано неправильно, это может привести к незначительным ошибкам или затруднить объяснение проблем с производительностью из-за неверных предположений при написании кода. Там есть компромисс.
Берин Лорич
17

Главным образом потому, что ленивый код и состояние могут плохо смешиваться и приводить к трудностям при поиске ошибок. Если состояние зависимого объекта изменяется, значение вашего ленивого объекта может быть неправильным при оценке. Намного лучше, чтобы программист явно кодировал объект, чтобы быть ленивым, когда он / она знает, что ситуация является подходящей.

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

Том Сквайрс
источник
Да, изменчивое состояние + ленивая оценка = смерть. Я думаю, что единственные моменты, которые я потерял в финале SICP, касались использования set!ленивого интерпретатора Scheme. > :(
Тихон Джелвис
3
«Ленивый код и состояние могут плохо сочетаться»: это действительно зависит от того, как вы реализуете состояние. Если вы реализуете его, используя совместно изменяемые переменные, и вы зависите от порядка оценки вашего состояния, чтобы быть согласованным, то вы правы.
Джорджио
14

Ленивые оценки не всегда лучше.

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

Хорошая вещь о ленивой оценке - это когда вы можете написать более четкий код; получение 10-го простого числа путем фильтрации списка бесконечных натуральных чисел и получения 10-го элемента этого списка является одним из наиболее кратких и ясных способов продолжения: (псевдокод)

let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)

Я полагаю, что было бы довольно трудно выразить вещи так кратко без лени.

Но лень не ответ на все вопросы. Начнем с того, что ленивость нельзя применять прозрачно при наличии состояния, и я считаю, что состояние не может быть обнаружено автоматически (если вы не работаете, скажем, в Haskell, когда состояние достаточно явное). Таким образом, в большинстве языков ленивость должна выполняться вручную, что делает вещи менее ясными и, таким образом, устраняет одно из больших преимуществ ленивого eval.

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

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

Алекс
источник
Было бы возможно для действительно умного компилятора значительно снизить накладные расходы. или даже воспользоваться ленью для дополнительной оптимизации?
Тихон Джелвис
3

Вот краткое сравнение плюсов и минусов нетерпеливой и ленивой оценки:

  • Жадная оценка:

    • Потенциальные накладные расходы на ненужную оценку вещей.

    • Беспрепятственный, быстрая оценка.

  • Ленивая оценка:

    • Нет ненужной оценки.

    • Накладные расходы при каждом использовании значения.

Итак, если у вас много выражений, которые никогда не нужно оценивать, ленивый лучше; тем не менее, если у вас никогда не будет выражения, которое не нужно оценивать, lazy - это просто накладные расходы.

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

Следовательно, ленивая оценка просто не окупается в среднем, более подходящая оценка лучше подходит для современного кода.

cmaster
источник
1
«Накладные расходы при каждом использовании значения.»: Я не думаю, что накладные расходы больше, чем, скажем, проверка на наличие нулевых ссылок в языке, подобном Java. В обоих случаях вам нужно проверить один бит информации (оцененный / ожидающий или нулевой / ненулевой), и вам нужно делать это каждый раз, когда вы используете значение. Так что да, накладные расходы есть, но они минимальны.
Джорджио
1
«Сколько написанных вами функций не требуют оценки всех своих аргументов?»: Это только один пример приложения. Как насчет рекурсивных, бесконечных структур данных? Можете ли вы реализовать их с нетерпением оценки? Вы можете использовать итераторы, но решение не всегда такое лаконичное. Конечно, вы, вероятно, не пропустите то, что вы никогда не имели возможности широко использовать.
Джорджио
2
«Следовательно, ленивая оценка просто не окупается в среднем, более подходящая оценка лучше подходит для современного кода». Это утверждение не выполняется: оно действительно зависит от того, что вы пытаетесь реализовать.
Джорджио
1
@Giorgio Вам может показаться, что накладные расходы невелики, но условные выражения - это одна из вещей, от которых страдают современные процессоры: неправильно предсказанная ветвь обычно приводит к полной очистке конвейера, отбрасывая работу более десяти циклов процессора. Вы не хотите ненужных условий в вашем внутреннем цикле. Оплата десяти циклов за каждый аргумент функции почти так же неприемлема для чувствительного к производительности кода, как и кодирование в Java. Вы правы в том, что ленивая оценка позволяет вам выполнить некоторые уловки, которые вы не можете легко сделать с нетерпеливой оценкой. Но подавляющее большинство кода не нуждается в этих хитростях.
Мастер
2
Это кажется ответом из неопытности с языками с ленивой оценкой. Например, как насчет бесконечных структур данных?
Андрес Ф.
3

Как отметил @DeadMG, Ленивая оценка требует затрат на ведение бухгалтерского учета. Это может быть дорого по сравнению с нетерпением оценки. Рассмотрим это утверждение:

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

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

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

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

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

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

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

BillThor
источник
1
Вы, кажется, спорите из-за неопытности. Я предлагаю вам прочитать статью «Почему функциональное программирование имеет значение» Wadler. Он посвящен основному разделу, объясняющему причину ленивой оценки (подсказка: он имеет мало общего с производительностью, ранней оптимизацией или «загрузкой редко используемых данных» и всем, что связано с модульностью).
Андрес Ф.
@AndresF Я прочитал газету, на которую вы ссылаетесь. Я согласен с использованием ленивых оценок в таких случаях. Ранняя оценка может быть неуместной, но я бы сказал, что возврат поддерева для выбранного хода может принести значительную выгоду, если легко добавить дополнительные ходы. Однако создание такой функциональности может быть преждевременной оптимизацией. За пределами функционального программирования у меня возникли серьезные проблемы с использованием отложенной оценки и невозможностью использования отложенной оценки. Есть сообщения о значительных затратах на производительность в результате ленивой оценки в функциональном программировании.
BillThor
2
Такие как? Существуют также отчеты о значительных затратах на производительность при использовании нетерпеливой оценки (затраты в форме либо ненужной оценки, либо отсутствия завершения программы). Приходится задуматься об этом, чтобы заплатить за любую другую (неправильно) используемую функцию. Сама модульность может стоить дорого; вопрос в том стоит ли оно того.
Андрес Ф.
3

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

[True, True, True, ... False]

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

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

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

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

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

tail_recursion
источник
«Итак, суть в том, что вы должны использовать все, что лучше для поставленной задачи». +1
Джорджио