Меня беспокоит эффективность ленивых вычислений Haskell. рассмотрите следующий код

main = print $ x + x
   where x = head [1..]

Здесь x сначала удерживает выражение head [1..] вместо результата 1 из-за лени, но тогда, когда я вызываю x + x, будет ли выражение head [1..] выполняться дважды?

Я нашел следующее описание на haskell.org

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

Значит ли это, что в x + x при вызове первого x выполняется head [1..] и x переназначается на 1, а второй x просто ссылается на это?

Я правильно понял?

3
Larry 23 Май 2014 в 08:07

3 ответа

Лучший ответ

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

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

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

На практике, однако, вы обычно довольно безопасны, предполагая, что всякий раз, когда вы даете выражению имя (как в where x = head [1..]), тогда все использования этого имени (в рамках привязки) будут ссылками на один преобразователь. .

11
amalloy 23 Май 2014 в 19:46

Сначала x - это просто преобразователь. Вы можете увидеть это следующим образом:

λ Prelude> let x = head [1..]
λ Prelude> :sprint x
x = _

Здесь _ указывает, что x еще не оценивался. Его простое определение записано.

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

Вы можете увидеть это с помощью ghc-vis:

λ Prelude> :vis
λ Prelude> :view x
λ Prelude> :view x + x

Должен показать вам что-то вроде:

thunk

Здесь вы можете видеть, что преобразователь x + x на самом деле дважды указывает на преобразователь x.

Теперь, если вы оцените x, напечатав его, например:

λ Prelude> print x

Вы получите:

evaluated

Вы можете видеть здесь, что преобразователь x больше не преобразователь: это значение 1.

9
m09 26 Май 2014 в 07:02

Есть два способа оценить выражение:

  1. Ленивый (сначала оцените самое внешнее).
  2. Строгий (сначала оцените самое сокровенное).

Рассмотрим следующую функцию:

select x y z = if x > z then x else y

Теперь давайте назовем это:

select (2 + 3) (3 + 4) (1 + 2)

Как это будет оцениваться?

Строгая оценка: оценивайте в первую очередь самое сокровенное.

select (2 + 3) (3 + 4) (1 + 2)

select 5 (3 + 4) (1 + 2)

select 5 7 (1 + 2)

select 5 7 3

if 5 > 3 then 5 else 7

if True then 5 else 7

5

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

Ленивая оценка: оцените в первую очередь самое внешнее.

select (2 + 3) (3 + 4) (1 + 2)

if (2 + 3) > (1 + 2) then (2 + 3) else (3 + 4)

if 5 > (1 + 2) then 5 else (3 + 4)

if 5 > 3 then 5 else (3 + 4)

if True then 5 else (3 + 4)

5

Ленивая оценка потребовала всего 5 сокращений. Мы никогда не использовали (3 + 4) и, следовательно, никогда не оценивали его. При ленивом вычислении мы можем оценить функцию, не оценивая ее аргументы. Аргументы оцениваются только при необходимости. Следовательно, функции вызываются «по необходимости».

Однако стратегии оценки «по требованию» требуют дополнительного учета - вам нужно отслеживать, было ли оценено выражение. В приведенном выше выражении, когда мы вычисляем x = (2 + 3), нам не нужно вычислять его снова. Однако нам нужно отслеживать, была ли она оценена.


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

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

3
Aadit M Shah 23 Май 2014 в 06:02