Что касается вашего последнего вопроса (почему Haskell делает это), причина, по которой «Strict Haskell» ведет себя иначе, чем действительно строгий язык, заключается в том, что расширение Strict
не меняет модель оценки с ленивой на строгую. Он просто делает подмножество привязок в «строгие» привязки по умолчанию, и только в ограниченном смысле Хаскелла принуждения оценки к нормальной форме слабой головы. Кроме того, это влияет только на привязки, сделанные в модуле с включенным расширением; это не оказывает обратного влияния на привязки, сделанные в других местах. (Более того, как описано ниже, строгость не вступает в силу при частичном применении функции. Функция должна быть полностью применена, прежде чем какие-либо аргументы будут принудительными.)
В вашем конкретном примере на Haskell я считаю, что единственный эффект расширения Strict
заключается в том, что вы явно написали следующие шаблоны взрыва в определении silly
:
silly !g !f = g (f (trace "evaluated" 0))
Это не имеет никакого другого эффекта. В частности, он не делает const
или (+)
строгими в своих аргументах, а также не изменяет семантику приложений функций, чтобы сделать их нетерпеливыми.
Таким образом, когда print
навязывает термин silly (const 0) (+)
, единственным эффектом является оценка его аргументов для WHNF как части приложения функции silly
. Эффект похож на запись (в не-Strict
Haskell):
let { g = const 0; f = (+) } in g `seq` f `seq` silly g f
Очевидно, что принуждение g
и f
к их WHNF (которые являются лямбдами) не будет иметь никакого побочного эффекта, и когда применяется silly
, const 0
все еще ленив в своем оставшемся аргументе, итоговый термин выглядит примерно так:
(\x -> 0) ((\x y -> <defn of plus>) (trace "evaluated" 0))
(что следует интерпретировать без расширения Strict
- здесь все ленивые привязки), и здесь нет ничего, что могло бы вызвать побочный эффект.
Как отмечалось выше, есть еще одна тонкая проблема, которую этот пример закрывает. Даже если вы все сделали строгим:
{-# LANGUAGE Strict #-}
import Debug.Trace
myConst :: a -> b -> a
myConst x y = x
myPlus :: Int -> Int -> Int
myPlus x y = x + y
silly :: ((Int -> Int) -> Int) -> (Int -> Int -> Int) -> Int
silly g f = g (f (trace "evaluated" 0))
main = print $ silly (myConst 0) myPlus
это все равно не напечатало бы "оценено". Это связано с тем, что при оценке silly
, когда строгая версия myConst
вынуждает свой второй аргумент, этот аргумент является частичным применением строгой версии myPlus
, и myPlus
не будет форсировать какую-либо его аргументы, пока он не будет полностью применен.
Это также означает, что если вы измените определение myPlus
на:
myPlus x = \y -> x + y -- now it will print "evaluated"
тогда вы сможете в значительной степени воспроизвести поведение ML. Поскольку myPlus
теперь полностью применен, он заставит свой аргумент, и это выведет «оцененный». Вы можете снова подавить это расширение eta f
в определении silly
:
silly g f = g (\x -> f (trace "evaluated" 0) x) -- now it won't
потому что теперь, когда myConst
заставляет свой второй аргумент, этот аргумент уже находится в WHNF (потому что это лямбда), и мы никогда не доберемся до применения f
, полного или нет.
В конце концов, я бы не стал слишком серьезно относиться к "Haskell плюс расширение Strict
и небезопасным побочным эффектам, таким как trace
", как к хорошей точке в области дизайна. Его семантика может быть (едва) последовательной, но они, конечно, странные. Я думаю, что единственный серьезный вариант использования - это когда у вас есть некоторый код, семантика которого «очевидно» не зависит от ленивых и строгих вычислений, но где производительность будет улучшена большим количеством форсирования. Затем вы можете просто включить Strict
для повышения производительности, не задумываясь.