Функция без баллов является общей, но оценивается дважды

Я пытался понять, как работает совместное вычисление в Haskell. Насколько я понимаю, общие вычисления без баллов должны оцениваться только один раз (любезно предоставлено CSE).

(A) Например, рассмотрим следующий код и его вывод:

*Main> let foo = trace "eval foo" 5 in foo + foo
eval foo
10

*Main> let foo' = \x -> trace "eval foo'" x in (foo' 5) + (foo' 5)
eval foo'
eval foo'
10

Как и ожидалось, foo оценивается только один раз (CSE, вероятно, начинает действовать), тогда как foo' оценивается лениво дважды. Это хорошо. Я попробовал вышеупомянутое использование GHCi, версия 7.6.3. Затем я попробовал тот же код в GHCi версии 8.6.5, но вместо этого получил следующий результат:

*Main> let foo = trace "eval foo" 5 in foo + foo
eval foo
eval foo
10

Обратите внимание, что foo оценивается дважды.

(B) Аналогично, с GHCi, версия 7.6.3:

*Main> let goo = const (trace "eval goo" 5) in goo () + goo ()
eval goo
10

но GHCi, версия 8.6.5, дважды оценивает goo:

*Main> let goo = const (trace "eval goo" 5) in goo () + goo ()
eval goo
eval goo
10

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

*Main> let foo_wrapper x = let foo = trace "eval foo" x in foo + foo
*Main> foo_wrapper 5
eval foo
10

Интересно, были ли некоторые оптимизации по умолчанию отключены в GHCi-8 или побочные эффекты от trace заставляют foo быть оценены как-то дважды? Или была проблема в GHCi-7? Как GHCi должен вести себя с такими выражениями, как (A) и (B)?

(Обновление 1)

Для сценария (C) рассмотрите следующие прогоны в GHCi-8 (с основным отличием во втором аргументе trace):

*Main> let foo_wrapper x = let foo = trace "eval foo" x in foo + foo 
*Main> foo_wrapper 5
eval foo
10
*Main> :t (foo_wrapper 5)
(foo_wrapper 5) :: Num a => a

*Main> let foo_wrapper' x = let foo = trace "eval foo" 5 in foo + foo 
*Main> foo_wrapper' ()
eval foo
eval foo
10
*Main> :t (foo_wrapper' ())
(foo_wrapper' ()) :: Num a => a

Ответы

Ответ 1

  Как и ожидалось, foo оценивается только один раз (CSE, вероятно, вступает в силу)

Нет, это не имеет ничего общего с CSE, просто работает ленивая оценка (так называемый вызов по необходимости): foo является константной аппликативной формой, поэтому ее просто нужно вычислить (принудительно из спасибо WHNF) один раз и затем может быть просто повторно использован без каких-либо дальнейших вычислений. Причина, по которой это больше не работает в GHCi-8, заключается в том, что 7.8 сняло ограничение мономорфизма в GHCi. Почему это актуально? Ну, trace "eval foo" 5 является полиморфным выражением типа Num a -> a. И полиморфные выражения не могут быть CAF. Таким образом, вместо вызова по требованию вы получаете семантику вызова по имени.

Самый простой способ снова получить общий доступ - это применить CAF, сделав тип мономорфным, добавив явную подпись:

Prelude Debug.Trace> let foo = trace "eval foo" 5 :: Int in foo + foo
eval foo
10