Подтвердить что ты не робот

Что означает "выплывание"?

На Haskell wiki я прочитал, что это:

fib =
    let fib' 0 = 0
        fib' 1 = 1
        fib' n = fib (n - 1) + fib (n - 2)
    in  (map fib' [0 ..] !!)

более эффективен, чем это:

 fib x =
    let fib' 0 = 0
        fib' 1 = 1
        fib' n = fib (n - 1) + fib (n - 2)
    in  map fib' [0 ..] !! x

Потому что "во втором случае fib" (re) определяется для каждого аргумента x, поэтому его нельзя выгрузить ".

Я не понимаю, что это значит.

  • Что означает "выплывание"? Как это оптимизация?
  • Почему fib' переопределяется для каждого вызова fib?
  • Является ли это расширением eta или нет?
4b9b3361

Ответ 1

Это не очень хорошее объяснение.

"Плавающий" просто означает, что в:

\x -> let y = ... in z

Если ... не упоминает x, то его можно выгрузить из лямбда:

let y = ... in \x -> z

что означает, что он будет вычисляться только один раз, 1 что может сэкономить много времени, если ... дорого. Тем не менее, GHC консервативен в отношении таких оптимизаций, поскольку они могут создавать утечки пространства. (Хотя он делает это для второго определения, если вы даете ему подпись типа, как указывает Даниэль Фишер в своем ответе.)

Однако речь идет не о автоматической оптимизации. Первый фрагмент определяет fib' вне лямбда, тогда как второй определяет его внутри (лямбда неявна в fib x = ..., что эквивалентно fib = \x -> ...), что и говорит цитата.

Даже это не очень актуально, однако; что имеет значение в первом фрагменте, map fib' [0 ..] происходит за пределами лямбда, и поэтому его результат разделяется между всеми приложениями лямбда (в этом коде "лямбда" возникает из частичного применения (!!)). В последнем он находится внутри лямбда и поэтому может быть пересчитан для каждого применения fib.

Конечным результатом является то, что первая реализация кэширует значения и поэтому намного эффективнее последней. Обратите внимание, что эффективность первого фрагмента зависит от того, что fib' не рекурсивно, а вместо этого через fib и, следовательно, извлекает выгоду из memoisation.

Это связано с eta-расширением; последний фрагмент является эта-расширением первого. Но выражение, которое вы цитируете, не объясняет, что происходит вообще.

1 Обратите внимание, что это поведение, специфичное для реализации, а не часть семантики Haskell. Однако все разумные реализации будут вести себя таким образом.

Ответ 2

ehird ответ объясняет все очень хорошо, однако там одна точка

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

что иногда бывает неправильным.

Если вы скомпилируете модуль, содержащий определение с оптимизацией (я проверил только -O2, а не -O1 и, конечно, только GHC), необходимо рассмотреть несколько случаев:

  • первое определение без сигнатуры типа
  • второе определение с сигнатурой типа fib :: Int -> Integer
  • первое определение с полиморфным типом fib :: Num a => Int -> a
  • второе определение без сигнатуры типа
  • второе определение с сигнатурой типа fib :: Num a => Int -> a

В случае 1 ограничение мономорфизма создает тип fib :: Int -> Integer, а список map fib' [0 .. ] является общим для всех вызовов fib. Это означает, что если вы когда-либо запрашиваете fib (10^6), у вас есть список первых миллионов чисел (Fibonacci) в памяти, и он будет собран только тогда, когда сборщик мусора может определить, что он больше не используется. Это часто утечка памяти.

В случае 2 результат (сердечник) практически идентичен случаю 1.

В случае 4 список не разделяется между различными вызовами верхнего уровня fib (конечно, результат может иметь много типов, поэтому было бы много списков для совместного использования), но он создавался один раз за каждый вызов верхнего уровня и повторное использование для вызовов из fib', поэтому для вычисления fib n требуется добавление O (n) и O (n ^ 2). Это не так уж плохо. Список собран, когда расчет завершен, поэтому нет утечки пространства.

Случай 3 практически идентичен 4.

Случай 5, однако, хуже наивной рекурсии. Поскольку он явно полиморфен и список связан внутри лямбда, список не может быть повторно использован для рекурсивных вызовов, каждый рекурсивный вызов создает новый список...