为什么这个功能没有触底
xs = [1, 2] ++ undefined
length $ take 2 $ take 4 xs
我的大脑正在读这个
length (take 2 ( take 4 xs ) )
如果括号中的所有内容都首先被评估,那么为什么这不会出现 Exception: prelude.undefined 的错误?
书中给出的答案是 take 2 只采用前两个索引,但是 take 4 不应该在这里优先并首先评估吗?
回答
如果括号中的所有内容都首先被评估,那么为什么这不会出错
Exception: prelude.undefined?
因为 Haskell 是懒惰的。这意味着它不会评估任何东西,除非有必要。
实际上,length将需要访问列表(但如果这些包含计算,则不需要访问其元素)。因此它会询问take 2 ( take 4 xs ) )第一个元素,然后是第二个,然后是第三个,依此类推。
现在take也很懒。这意味着只要它不需要评估,它就什么都不做。如果length询问下一个元素,它将确定该元素,如果稍后询问另一个元素,它将为列表提供第二个元素。如果length请求另一个,take 2则将停止,从而返回空列表。
因此,Haskell 没有像 Python 或 Java 那样首先评估最内层函数并从左到右的评估策略。
回答
让我们计算一下!什么是[1, 2] ++ undefined?好吧,要知道这一点,我们首先需要知道是什么[1, 2]。它实际上只是1:2:[].
接下来,我们需要知道什么++是。在源代码中告诉我们:
(++) :: [a] -> [a] -> [a]
[] ++ ys = ys
(x : xs) ++ ys = x : xs ++ ys
好的。所以让我们模式匹配。
(1:2:[]) ++ undefined
= -- the second pattern matches
1 : (2:[]) ++ undefined
= -- the second pattern matches
1 : 2 : [] ++ undefined
= -- the first pattern matches
1 : 2 : undefined
好的,所以我们已经弄清楚了
xs = 1 : 2 : undefined
怎么样take 4 xs?那么,我们首先要看看是什么take。出于性能原因,真正的源代码有些复杂,但我们可以使用这个更简单、等效的版本:
take :: Int -> [a] -> [a]
take n _ | n <= 0 = []
take _ [] = []
take n (a : as) = a : take (n - 1) as
所以
take 4 xs
= -- value of xs
take 4 (1:2:undefined)
= -- 4 is positive, so the first guard falls through.
-- the second pattern fails (the list isn't empty)
-- the third pattern succeeds
1 : take 3 (2:undefined)
= -- same analysis
1 : 2 : take 3 undefined
= -- 3 is positive, so the guard fails.
-- the pattern match on undefined produces undefined
1 : 2 : undefined
简直是一样的xs!
接下来我们计算
take 2 (take 4 xs)
= -- as shown above
take 2 (1 : 2 : undefined)
= -- following the same sequence above
1 : take 1 (2 : undefined)
= -- same same
1 : 2 : take 0 undefined
= -- this time the guard succeeds!
1 : 2 : []
= -- reinstalling syntactic sugar
[1, 2]
所以现在你有一个完全定义的两个元素的列表,并且获取它的长度根本没有什么特别的挑战。
注意以上是计算。它不代表程序运行时发生的实际操作顺序。但是……这实际上并不重要。Haskell 的一大优点是,在您的分析中,您可以随时“用等号替换等号”。不会影响结果。但是,您必须小心一点,不要让它迷惑您。例如,假设您要评估null (repeat ()). 你会开始这样的事情:
repeat () = () : repeat ()
下一步你要怎么做?好吧,一种选择是继续扩展repeat ():
repeat () = () : () : repeat ()
这是完全有效的。但如果你一直这样下去,你永远得不到答案。在某些时候,您必须查看问题的其余部分。
null (() : repeat ())
是立即False。所以仅仅因为你可以找到一个无限的减少序列并不意味着有一个无限循环。事实上,按照 Haskell 的工作方式,如果每一个可能的减少序列都是无限的,那么只有一个无限循环。