我们知道$运算符绑定最松散,并且也与右侧相关联,这意味着,[1 ..]应该首先进行评估,因此,它不应该遇到无限循环?为什么它甚至停止了?
答案 0 :(得分:8)
Haskell很懒惰,($)
不会改变它。 ($)
运算符并不神奇,它是一个完全普通的Haskell函数†:
($) :: (a -> b) -> a -> b
f $ x = f x
由于Haskell是惰性的,因此在将参数传递给函数之前不会对其进行求值,($)
也不例外。因此,take 2 $ [1..]
与(take 2) [1..]
相同,当然与take 2 [1..]
相同。没有进行额外的评估。
现在,事实证明, 是($)
的严格版本($!)
,它在应用函数之前将其参数计算为weak head normal form (WHNF)。它也可以定义为普通的Haskell函数,但它必须使用神奇的seq
函数作为其定义的一部分:
($!) :: (a -> b) -> a -> b
f $! x = x `seq` f x
但是,即使take 2 $! [1..]
也会产生[1,2]
,而不是分歧。为什么?好吧,$!
只评估其对WHNF的参数,不正常形式,并且WHNF可以被认为是“浅层”评估。它评估了第一个缺点,但仅此而已。您可以使用GHCi中的:sprint
命令来查看:
ghci> let xs = [1..] :: [Int]
ghci> xs `seq` ()
()
ghci> :sprint xs
xs = 1 : _
要递归强制一个值,您需要使用the deepseq
package,顾名思义,它会深入评估一个值。它提供了($)
的“{更强”版本,称为($!!)
,与($!)
类似,但使用deepseq
代替seq
。因此,take 2 $!! [1..]
实际上是分歧的。
†自there are some special typing rules in the compiler to help check idiomatic uses of $
when used with higher-rank types以来,GHC中的严格为真。但是,这里没有任何相关内容,更简单的定义同样适用。
答案 1 :(得分:6)
为了补充其他答案,请允许我补充说,您对评估顺序(或评估策略)和优先级感到困惑。这是一种经常出现的误解。
举例说明,请考虑表达式
f 0 * g 0 + h 0
优先级告诉我们必须在加法之前执行乘法运算。但是,这并不意味着必须在f 0
之前评估g 0
和h 0
!编译器可以选择首先计算h 0
,然后g 0
,然后计算f 0
,最后再乘以,然后添加。
这不仅适用于Haskell,甚至适用于像C这样的命令式语言,它们不指定评估顺序并允许函数产生副作用。
除此之外,您还必须了解在Haskell中“评估”某些内容,大致意味着在第一个构造函数出现之前对其进行评估(WHNF)。因此,评估[1..]
会大致导致1 : [2..]
必须评估尾部的位置。如果评估[1..]
会导致无限循环,那么在表达式中根本不可能使用[1..]
:只能选择丢弃它而不进行评估,或永远循环。
答案 2 :(得分:4)
$
在这里是一个红鲱鱼。 take 2 $ [1..]
与take 2 [1..]
完全相同。 $
只会影响什么是对什么的论证;它对评估事物没有任何影响。
(例如:
print 2 + 2 ==> (print 2) + 2 {- Doesn't work. -}
print $ 2 + 2 ==> print (2 + 2) {- Works. -}
美元影响print
是+
的论证还是相反。美元本身并没有评估任何东西。)
"最顶级"这里的函数是take
,所以我们首先评估它。 take
的定义可以这样写:
take 0 xs = xs
take n xs =
case xs of
x : xs' -> x : take (n-1) xs'
[] -> []
假设长度不为零,那么首先要做的是case xs of ...
,这意味着必须对xs
(在这种情况下为[1..]
)进行评估以确定它是否为&{0}} #39; sa :
或[]
。这样做,我们发现{在恒定时间内)xs = 1 : [2..]
,因此第一种情况适用。
你可以这样写出来......
take 2 [1..]
take 2 (1 : [2..])
1 : take (2-1) [2..]
1 : take 1 [2..]
1 : take 1 (2 : [3..])
1 : 2 : take (1-1) [3..]
1 : 2 : take 0 [3..]
1 : 2 : []
(我仍然认为很遗憾没有人提出自动生成此类痕迹的工具......它可能会让一些人感到困惑,并且可能非常适合调试......)
答案 3 :(得分:2)
由于函数应用程序具有最高优先级,因此表达式将被解析为(take 2) $ [1..]
。这意味着您首先获得一个函数(take 2)
,然后将其应用于参数[1..]
。
然而,这一切都无关紧要,因为Haskell是一种懒惰的语言。你可以反过来写它并得到完全相同的结果:
> [1..] & take 2
[1.2]
(&)
是($)
运算符的反转版本。
即使[1..]
首先在此处,也不会在需要其内容之前对其进行评估。