Функция без баллов является общей, но оценивается дважды
Я пытался понять, как работает совместное вычисление в 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