【问题标题】:Why is this evaluated like this?为什么会这样评价?
【发布时间】:2014-04-11 11:11:54
【问题描述】:

我有这个功能

doubleMe :: Num a => [a] -> [a]
doubleMe [] = []
doubleMe (x:xs) = (2*x):(doubleMe xs)

考虑一下:

doubleMe(doubleMe([1,2,3]))

第一步显然是

doubleMe(doubleMe([1,2,3])) = doubleMe(2:doubleMe[2,3])

因为没有其他可能。

这是我想知道的下一步。为什么是

doubleMe(2:doubleMe[2,3]) = 4:(doubleMe(doubleMe([2,3])))

而不是

doubleMe(2:doubleMe[2,3]) = doubleMe(2:4:doubleMe([3]))

?

到目前为止,我能想出的唯一答案是

因为它有意义。否则 Haskell 不会在列表上懒惰地行动。

但这不是答案,而是逃避。真正的答案是什么?

【问题讨论】:

  • 其实第一步是doubleMe(doubleMe([1,2,3])) -> doubleMe((2*1) : doubleMe [2,3])(2*1) 在出现之前不会被强迫(如果有的话!)。

标签: haskell


【解决方案1】:

第一步明显是doubleMe(doubleMe([1,2,3])) = doubleMe(2:doubleMe[2,3])

其实不是很明显!

为了便于区分,

doubleMe' = doubleMe

并考虑

doubleMe' $ doubleMe [1,2,3]

第一步如你所说的唯一原因,即

≡ doubleMe' $ 2 : doubleMe [2,3]

是因为doubleMe' 需要能够将其参数与[]_:_ 匹配。为此,运行时开始稍微评估doubleMe [1,2,3],即一个递归步骤。这会产生2 : doubleMe [2,3],足以让doubleMe' 使用:

≡ 4 : doubleMe' (doubleMe [2,3])

请注意,实际上这种评估顺序并不是语言所要求的:编译器可以对其重新排序(例如,出于性能原因),因此实际上内部列表会立即被完全处理 - 如果它可以证明这不会改变语义,即对于一个无限列表,doubleMe [1..] 一定不能陷入一个永恒的循环,如果你只要求前几个结果。

GHC 不进行这种重新排序。

【讨论】:

  • 是的,这是正确的方法。 :) 没有考虑过惰性求值。
  • 请在learnyouahaskell.com/introduction 搜索此文本:“但是,一旦你想看到结果,第一个 doubleMe 告诉第二个它现在想要结果!第二个告诉第三个第三个不情愿地返回一个加倍的 1,这是一个 2。第二个收到它并将 4 返回给第一个。第一个看到并告诉你第一个元素是 8。所以它只通过一次通过列表,并且仅在您真正需要时。”
  • 您似乎在说的是,很可能整个列表被计算并返回了三次,而不是一次只发生一个元素。您似乎在说我们不一定对订单一无所知。那么,那篇文章的作者有错吗?随意扩展您的答案。
  • 我对列表返回三次说了什么??当然不是,那会很糟糕。但事实上,我们对确切的顺序一无所知,尽管可能它会像描述的那样完全是懒惰的。问题是:你真的不应该关心评估顺序,因为只要你得到正确的结果,计算步骤的排序方式并不重要(这就是引用透明度的重点等)非严格评估(正式名称)保证:无论多长时间,您都不必永远等待仅弹出列表中的某些元素。
【解决方案2】:

需要意识到的重要一点是 case 并且只有 case 会导致 Haskell[1] 中的评估。因此,当你说“考虑一下:”

doubleMe(doubleMe([1,2,3]))

说出我们正在考虑的什么非常重要。 Haskell[1] 中函数应用的副作用不会发生求值。唯一可以导致(部分)它被评估的是case 语句,它的模式匹配它。那么case 在这里是如何工作的呢?

case doubleMe (doubleMe [1,2,3]) of
    []     -> ...
    x : xs -> ... x ... xs ...

进行如下。我们必须对函数调用的返回值进行模式匹配,因此我们将函数调用替换为其主体(取消函数参数模式匹配到case

case (case doubleMe [1,2,3] of
          []   -> []
          x:xs -> (2*x) : doubleMe xs)
     ) of
    []     -> ...
    x : xs -> ... x ... xs ...

我们已经引入了 second 案例,因此会导致评估

case (case (case [1,2,3] of
                 []   -> []
                 x:xs -> (2*x) : doubleMe xs
           ) of
          []   -> []
          x:xs -> (2*x) : doubleMe xs)
     ) of
    []     -> ...
    x : xs -> ... x ... xs ...

现在我们有了第三个case。这个直接匹配数据结构,因此它立即返回并选择要遵循的适当分支(即:)绑定x1xs[2,3]

case (case ((2*1) : doubleMe [2,3]
           ) of
          []   -> []
          x:xs -> (2*x) : doubleMe xs)
     ) of
    []     -> ...
    x : xs -> ... x ... xs ...

现在第二个case 的审查员已被评估,因此它可以继续选择适当的分支(同样是:)绑定x(2*1)xsdoubleMe [2,3]

case ((2*(2*1)) : doubleMe (doubleMe [2,3]))
     ) of
    []     -> ...
    x : xs -> ... x ... xs ...

最后原来的case可以选择它的分支

... (2*(2*1)) ... doubleMe (doubleMe [2,3])) ...

下一个问题是如何评估(2*(2*1))doubleMe (doubleMe [2,3])) 术语。答案是它们只能作为更高级别的case 的结果来强制执行,该级别会仔细检查它们所属的表达式。


[1] 或者更确切地说是在 Haskell 的 实现 中。原则上 Haskell 可以以不同的方式实现,但我所知道的所有版本都是这样做的。

【讨论】:

    猜你喜欢
    • 2018-10-15
    • 2012-08-15
    • 2012-09-13
    • 2011-06-03
    • 2018-06-08
    • 2014-12-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多