Haskell如何在$ 1中工作?
我们知道$运算符绑定最松散的,也是右边的关联,这意味着应该先评估[1 ..],因此它不应该陷入无限循环? 为什么它甚至停下来呢?
$
是这里的一条红鲱鱼。 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..]
)以决定它是a :
还是a []
。 这样做,我们发现(在不变的时间) 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 : []
(我仍然觉得可惜没有人提出自动生成这样的痕迹的工具......它可能会让一些人不感冒,并且可能对调试非常有用......)
Haskell是懒惰的, ($)
不会改变它。 ($)
运算符根本不是神奇的,它是一个完全普通的Haskell函数†:
($) :: (a -> b) -> a -> b
f $ x = f x
由于Haskell是懒惰的,参数在传递给函数之前不会被评估,并且($)
也不例外。 因此, take 2 $ [1..]
等于(take 2) [1..]
,这当然与take 2 [1..]
。 没有额外的评估发生。
现在,事实证明,有一个严格的($)
被称为($!)
,它在应用函数之前将其参数评估为弱头标准形式(WHNF)。 它也可以定义为一个普通的Haskell函数,但它必须使用神奇的seq
函数作为其定义的一部分:
($!) :: (a -> b) -> a -> b
f $! x = x `seq` f x
但是,即使take 2 $! [1..]
take 2 $! [1..]
会产生[1,2]
,而不是分歧。 为什么? 那么, $!
只是将其论证评估为WHNF,而不是正常形式,并且WHNF可以被认为是“浅”评估。 它评估了第一个缺点,但没有更多。 你可以在GHCi中使用:sprint
命令来看到这一点:
ghci> let xs = [1..] :: [Int]
ghci> xs `seq` ()
()
ghci> :sprint xs
xs = 1 : _
要递归地强制一个值,你需要使用deepseq
包,顾名思义,它深度评估一个值。 它提供了一个甚至更强大的($)
版本,称为($!!)
,就像($!)
但是使用deepseq
而不是seq
。 所以, take 2 $!! [1..]
事实上, take 2 $!! [1..]
将会发生分歧。
†这在GHC中并不严格,因为在编译器中有一些特殊的输入规则可以帮助检查用于更高级别类型的$
惯用用法。 然而,在这里没有任何关联,简单的定义工作原理是一样的。
为了补充其他答案,让我补充一点,你很困惑评估顺序(或评估策略)和优先顺序。 这是一种常见的误解。
为了举例说明,考虑表达式
f 0 * g 0 + h 0
优先级告诉我们,乘法必须在加法之前执行。 然而,这并不意味着必须在h 0
之前评估f 0
和g 0
h 0
! 编译器可以选择首先计算h 0
,然后g 0
,然后f 0
,最后再乘以,然后加。
这不仅适用于Haskell,即使是像C这样的命令式语言也没有规定评估顺序,并且允许函数产生副作用。
除此之外,你还必须明白,“评估”Haskell中的某些东西,大致意味着评估它,直到它的第一个构造函数出现(WHNF)。 因此,评估[1..]
结果大致为1 : [2..]
尾部必须被评估。 如果评估[1..]
会导致无限循环,那么在表达式中就没有办法使用[1..]
:只能选择丢弃它而不评估它,或者永远循环。