"foo" 是 "a -> b -> c"。 "foo . foo" 也是 "a -> b -> c"。 Haskell 在速度方面是否完全相同?

"foo" is "a -> b -> c". "foo . foo" is also "a -> b -> c". Does Haskell treat them exactly the same in terms of speed?

foo :: a -> b -> c
foo = undefined

ghci> :t foo
foo :: a -> b -> c

ghci> :t foo . foo
foo . foo :: a -> b -> c

ghci> :t foo . foo . foo
foo . foo . foo :: a -> b -> c

以此类推

类型相同。我们还清楚地看到,仅通过查看类型,这三者都是等价的。他们有相同的结果。

但是速度呢?例如,foo 是否会自动(没有优化)运行 与 . 组合一百万次,就像 foo 一样快?如果不是,优化(O1 或 O2)是否会如此?

是的,他们表现相同,但只是偶然。在 ghci:

> :i .
(.) :: (b -> c) -> (a -> b) -> (a -> c)     -- Defined in ‘GHC.Base’
infixr 9 .

因为它是 infixrfoo . foo . foo 解析为 foo . (foo . foo),所以我们可以用 foldr 快捷地写十亿次 foo . foo . foo . ...。所以:

> let foo :: a -> b -> c; foo = undefined
> foldr (.) id (replicate 1000000000 foo) ()
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
  undefined, called at <interactive>:1:5 in interactive:Ghci1

几乎是立即 returns。另一方面,下面两个挂了一会儿:

> foldl (.) id (replicate 1000000000 foo) ()
> foldr (flip (.)) id (replicate 1000000000 foo) ()

不过,我必须再次强调,这是GHC实现的巧合,而不是语言语义的保证。碰巧 GHC 的 imprecise exceptions 实现很快注意到在 foldr (.) 的情况下这将是一个错误(并抛出最左边的 foo 的错误),但不会尽快通知 foldl (.)。当然,在 foo 不仅是一个错误,而且必须进行一些实际计算的情况下,该计算将需要执行与它在组合字符串中出现的次数一样多的次数——GHC 非常了不起, 但并不神奇。