it-swarm.com.ru

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

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

Примечание: я не имею в виду памятки. 

110
Joel McCracken

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

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

90
mipadi

Полезным примером ленивой оценки является использование quickSort:

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

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

minimum ls = head (quickSort ls)

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

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

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

69
Chris Eidhof

Я нахожу ленивую оценку полезной для многих вещей. 

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

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

foo x = x + 3

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

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

В-третьих, лень позволяет вам писать очень функциональный код, который можно понять по частям. В 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 Monads. (Вы можете определить монаду на строгом языке, но, к сожалению, вы часто будете нарушать закон монады или два из-за отсутствия лени и рабочих процессов, для сравнения заберите тонну строгого багажа.)

62
Edward KMETT

Есть разница между обычной оценкой заказа и ленивой оценкой (как в 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

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

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

28
Thomas Danecker

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

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

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

25
Alexey

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

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

25
Norman Ramsey

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

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

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

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

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

13
Paul Johnson

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

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

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

12
Edward Z. Yang

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

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

9
yfeldblum
  1. Это может повысить эффективность. Это выглядит очевидным, но на самом деле это не самое главное. (Также обратите внимание, что лень тоже может kill эффективность - этот факт не сразу очевиден. Однако, сохраняя множество временных результатов, а не вычисляя их сразу, вы можете использовать огромное количество оперативной памяти.)

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

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

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

8
MathematicalOrchid

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

Приведенная выше структура данных не требует пересчета узлов при каждом обходе, поэтому они заметно отличаются от Vanilla IEnumerables в .NET.

6
Juliet

Учти это:

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

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

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

6
Romain Linsolas

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

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

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

5
ertes

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

В 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, вы можете либо выбросить руки в воздух и сдаться, либо это расширить и загромождать вещи. (Это очень похоже на то, что Ной говорил о том, что library строгая/ленивая, а не язык).

Теперь представьте, что вы пишете ту же функцию в строгом 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)
4
Owen

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

Давайте предположим, что мыМОЖЕМдолжны использовать 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)
4
Vinko Vrsalovic

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

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

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

3
Noah Lavine

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

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }
2
peeles

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

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
Marc Bernier

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

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

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

2
shapr

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

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

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


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

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

2
Will Ness

Если под "ленивой оценкой" вы подразумеваете как в combound booleans, как в 

   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, и все же, когда вам нужен супервизор, первый вызов свойства супервизора вызовет вызов базы данных, и данные будут извлечены и доступны ... 

1
Charles Bretana

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

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

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

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

0
onmyway133