我有两个 Haskell 函数,这两个函数看起来都与我非常相似。但是第一个对无限列表失败,第二个对无限列表成功。我已经尝试了几个小时来确切地确定这是为什么,但无济于事。
这两个片段都是 Prelude 中“单词”功能的重新实现。两者都适用于有限列表。
这是不处理无限列表的版本:
myWords_FailsOnInfiniteList :: String -> [String]
myWords_FailsOnInfiniteList string = foldr step [] (dropWhile charIsSpace string)
where
step space ([]:xs) | charIsSpace space = []:xs
step space (x:xs) | charIsSpace space = []:x:xs
step space [] | charIsSpace space = []
step char (x:xs) = (char : x) : xs
step char [] = [[char]]
这是处理无限列表的版本:
myWords_anotherReader :: String -> [String]
myWords_anotherReader xs = foldr step [""] xs
where
step x result | not . charIsSpace $ x = [x:(head result)]++tail result
| otherwise = []:result
注意:“charIsSpace”只是对 Char.isSpace 的重命名。
以下解释器会话说明第一个对无限列表失败,而第二个成功。
*Main> take 5 (myWords_FailsOnInfiniteList (cycle "why "))
*** Exception: stack overflow
*Main> take 5 (myWords_anotherReader (cycle "why "))
["why","why","why","why","why"]
编辑:感谢下面的回复,我相信我现在明白了。这是我的结论和修改后的代码:
结论:
- 我第一次尝试的最大罪魁祸首是两个以“step space []”和“step char []”开头的方程。将 step 函数的第二个参数与 [] 匹配是不可以的,因为它强制评估整个第二个参数(但需要在下面解释一个警告)。
- 有一次,我认为 (++) 可能会比 cons 更晚地评估它的右手参数,不知何故。所以,我想我可以通过将“= (char:x):xs”更改为“= [char:x] ++ xs”来解决这个问题。但那是不正确的。
- 在某一时刻,我认为将第二个 arg 与 (x:xs) 匹配的模式会导致该函数在无限列表中失败。我几乎是对的,但并不完全正确!正如我在上面的模式匹配中所做的那样,针对 (x:xs) 评估第二个 arg将导致一些递归。它会“转动曲柄”,直到碰到“:”(又名“缺点”)。如果这从未发生过,那么我的函数将无法针对无限列表成功。但是,在这种特殊情况下,一切正常,因为我的函数最终会遇到一个空格,此时会出现“cons”。并且通过匹配 (x:xs) 触发的评估将停在那里,避免无限递归。此时,“x”将被匹配,但 xs 仍将是一个 thunk,所以没有问题。(感谢 Ganesh 真正帮助我理解这一点)。
- 一般来说,您可以随意提及第二个 arg,只要您不强制评估它。如果你匹配了 x:xs,那么你可以随意提及 xs,只要你不强制评估它。
所以,这是修改后的代码。我通常尽量避免 head 和 tail,仅仅因为它们是偏函数,也因为我需要练习编写模式匹配等价物。
myWords :: String -> [String]
myWords string = foldr step [""] (dropWhile charIsSpace string)
where
step space acc | charIsSpace space = "":acc
step char (x:xs) = (char:x):xs
step _ [] = error "this should be impossible"
这对无限列表正确有效。请注意,看不到头、尾或 (++) 运算符。
现在,有一个重要的警告: 当我第一次编写更正的代码时,我没有与“step_[]”匹配的第三个等式。结果,我收到了有关非详尽模式匹配的警告。显然,避免该警告是个好主意。
但我以为我会遇到问题。我已经在上面提到过,将第二个 arg 与 [] 进行模式匹配是不行的。但我必须这样做才能摆脱警告。
但是,当我添加“step_[]”等式时,一切都很好!无限列表仍然没有问题!. 为什么?
因为从未达到更正代码中的第三个等式!
事实上,考虑以下 BROKEN 版本。它与正确的代码完全相同,只是我将空列表的模式移到了其他模式之上:
myWords_brokenAgain :: String -> [String]
myWords_brokenAgain string = foldr step [""] (dropWhile charIsSpace string)
where
step _ [] = error "this should be impossible"
step space acc | charIsSpace space = "":acc
step char (x:xs) = (char:x):xs
我们回到堆栈溢出,因为调用 step 时发生的第一件事是解释器检查第一个等式是否匹配。为此,它必须查看第二个参数是否为 []。为此,它必须评估第二个参数。
将方程向下移动到其他方程的下方可确保永远不会尝试第三个方程,因为第一个或第二个模式总是匹配。第三个等式只是为了省去非详尽的模式警告。
这是一次很棒的学习经历。感谢大家的帮助。