如何在haskell中工作2 $ [1 ..]?

时间:2017-07-24 08:20:00

标签: haskell

我们知道$运算符绑定最松散,并且也与右侧相关联,这意味着,[1 ..]应该首先进行评估,因此,它不应该遇到无限循环?为什么它甚至停止了?

4 个答案:

答案 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 0h 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..]首先在此处,也不会在需要其内容之前对其进行评估。