Haskell 为什么这样评价?

Haskell 为什么这样评价?,haskell,Haskell,我有这个功能 doubleMe :: Num a => [a] -> [a] doubleMe [] = [] doubleMe (x:xs) = (2*x):(doubleMe xs) 考虑这一点: doubleMe(doubleMe([1,2,3])) doubleMe(doubleMe([1,2,3])) 第一步显然是 doubleMe(doubleMe([1,2,3])) = doubleMe(2:doubleMe[2,3]) 因为没有其他的可能性 这是我想知道的下一

我有这个功能

doubleMe :: Num a => [a] -> [a]
doubleMe [] = []
doubleMe (x:xs) = (2*x):(doubleMe xs)
考虑这一点:

doubleMe(doubleMe([1,2,3]))
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]))
?

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

因为这很有道理。否则哈斯克尔就不会在名单上偷懒了

但这不是一个答案,而是一个错误。真正的答案是什么

第一步显然是
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不会进行这种重新排序。

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

说一下我们正在考虑的事情是非常重要的。在Haskell[1]中,评估不会产生函数应用的副作用。唯一能导致(部分)计算它的是一个模式匹配的
case
语句。那么
case
在这里是如何工作的呢

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

我们引入了第二个案例,因此需要进行评估

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 ...
现在我们有第三个
案例
。这一个直接匹配数据结构,因此它立即返回并选择要遵循的适当分支(即
)将
x
绑定到
1
xs
绑定到
[2,3]

case (case ((2*1) : doubleMe [2,3]
           ) of
          []   -> []
          x:xs -> (2*x) : doubleMe xs)
     ) of
    []     -> ...
    x : xs -> ... x ... xs ...
case ((2*(2*1)) : doubleMe (doubleMe [2,3]))
     ) of
    []     -> ...
    x : xs -> ... x ... xs ...
现在,第二个
案例的scrutinee已经过评估,因此它可以继续选择合适的分支(同样是
)绑定
x
(2*1)
xs
doubleMe[2,3]

case (case ((2*1) : doubleMe [2,3]
           ) of
          []   -> []
          x:xs -> (2*x) : doubleMe xs)
     ) of
    []     -> ...
    x : xs -> ... x ... xs ...
case ((2*(2*1)) : doubleMe (doubleMe [2,3]))
     ) of
    []     -> ...
    x : xs -> ... x ... xs ...
最后,原始的
案例
可以选择其分支

... (2*(2*1)) ... doubleMe (doubleMe [2,3])) ...
下一个问题是如何评估
(2*(2*1))
doubleMe(doubleMe[2,3])
术语。答案是,只有在更高级别的
案例检查它们所属的表达式时,它们才能被强制执行



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

是的,这是正确的方式。:)未考虑延迟评估。请搜索以下文本:“但是一旦你想看到结果,第一个doubleMe会告诉第二个它想要结果,现在!第二个告诉第三个,第三个不情愿地返回一个加倍的1,也就是2。第二个收到后,将4返回给第一个。第一个元素看到了,并告诉你第一个元素是8。因此,只有当你真正需要它时,它才会通过列表。“您似乎想说的是,整个列表很可能被计算并返回三次,而不是一次只发生一个元素。你好像在说我们不一定对订单有任何了解。那么,这篇文章的作者错了吗?请随意扩展您的答案。关于列表被返回三次,我说了些什么??当然不是,那将是非常糟糕的。但事实上,我们对确切的顺序一无所知,尽管它很可能会像上面所描述的那样是完全懒惰的。问题是:你真的不应该太在意计算顺序,因为只要你得到正确的结果,计算步骤的顺序就无关紧要了(这是引用透明性的问题等等),什么是非严格的计算(官方称之为)保证:您不必永远等待列表中的某些元素,不管有多长时间。实际上,第一步是
doubleMe(doubleMe([1,2,3])->doubleMe((2*1):doubleMe[2,3])
(2*1)
在到达前端之前不会被强制(如果有的话!)。