为什么第一个Haskell函数FAIL处理无限列表,而第二个片段SUCCEEDS带有无限列表?

时间:2009-05-12 04:00:03

标签: haskell functional-programming fold

我有两个Haskell函数,这两个函数看起来与我非常相似。但是第一个FAILS反对无限列表,第二个反对无限列表成功。我一直在努力确定原因,但无济于事。

两个片段都是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"]

编辑:感谢下面的回复,我相信我现在明白了。以下是我的结论和修订后的代码:

结论:

  1. 我第一次尝试的最大罪魁祸首是以“step space []”和“step char []”开头的2个方程式。 将步骤函数的第二个参数与[]匹配是一个禁忌,因为它会强制评估整个第二个参数(但需要注意以下解释)。
  2. 有一次,我曾想过(++)可能会稍后评估它的右手论证,而不是以某种方式。所以,我想我可以通过将“=(char:x):xs”更改为“= [char:x] ++ xs”来解决问题。但那不正确
  3. 有一次,我认为将第二个arg与(x:xs)匹配的模式会导致函数对无限列表失败。关于这个,我几乎是,但不完全正确!评估第二个arg(x:xs),就像我在上面的模式匹配中一样, WILL 会导致一些递归。它将“转动曲柄”直到它击中“:”(又名“cons”)。如果这种情况从未发生过,那么我的函数就无法在无限列表中成功。但是,在这种特殊情况下,一切都很好,因为我的函数最终会遇到一个空格,此时会出现“缺点”。通过匹配(x:xs)触发的评估将在那里停止,避免无限递归。此时,“x”将匹配,但xs仍然是一个thunk,所以没有问题。 (感谢Ganesh帮我理解这一点。)
  4. 一般来说,只要你没有强制评估它,你就可以提及你想要的第二个arg。如果你匹配x:xs,那么只要你不强制评估它就可以提到你想要的xs。
  5. 所以,这是修改后的代码。我通常试图避免头部和尾部,仅仅因为它们是部分功能,还因为我需要练习编写相应的模式匹配。

    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"
    

    这适用于无限列表。请注意,看不到头部,尾部或(++)操作员。

    现在,有一个重要的警告: 当我第一次写出修正后的代码时,我没有第3个等式,它与“step _ []”匹配。结果,我收到了关于非详尽模式匹配的警告。显然,避免这种警告是个好主意。

    但我以为我会遇到问题。我上面已经提到将第二个arg与[] 进行模式匹配是不行的。但我必须这样做才能摆脱警告。

    但是,当我添加“step _ []”等式时,一切都很好!无限列表仍然没有问题!。为什么?

    因为修正后的代码中的第3个等式从未到达过!

    实际上,请考虑以下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时发生的第一件事是解释器检查方程式1是否匹配。为此,必须查看第二个arg是否为[]。要做到这一点,它必须评估第二个arg。

    将等式向下移动到其他等式以下确保从不尝试第3个等式,因为第一个或第二个模式始终匹配。第三个等式仅仅是为了省去非详尽模式警告。

    这是一次很棒的学习经历。感谢大家的帮助。

4 个答案:

答案 0 :(得分:7)

尝试手动扩展表达式:

 take 5 (myWords_FailsOnInfiniteList  (cycle "why "))
 take 5 (foldr step [] (dropWhile charIsSpace (cycle "why ")))
 take 5 (foldr step [] (dropWhile charIsSpace ("why " ++ cycle "why ")))
 take 5 (foldr step [] ("why " ++ cycle "why "))
 take 5 (step 'w' (foldr step [] ("hy " ++ cycle "why ")))
 take 5 (step 'w' (step 'h' (foldr step [] ("y " ++ cycle "why "))))

下一次扩张是什么?您应该看到为了模式匹配step,您需要知道它是否为空列表。为了找到答案,你必须至少对它进行评估。但是第二个术语恰好是foldr减少了你模式匹配的函数。换句话说,step函数无法在不调用自身的情况下查看其参数,因此您可以进行无限递归。

与第二个功能的扩展形成对比:

myWords_anotherReader (cycle "why ")
foldr step [""] (cycle "why ")
foldr step [""] ("why " ++ cycle "why ")
step 'w' (foldr step [""] ("hy " ++ cycle "why ")
let result = foldr step [""] ("hy " ++ cycle "why ") in
    ['w':(head result)] ++ tail result
let result = step 'h' (foldr step [""] ("y " ++ cycle "why ") in
    ['w':(head result)] ++ tail result

您可能会看到此扩展将持续到达到一个空格。达到空格后,“头部结果”将获得一个值,您将产生答案的第一个元素。

我怀疑第二个函数会溢出不包含任何空格的无限字符串。你能明白为什么吗?

答案 1 :(得分:7)

其他人已经指出了这个问题,即在生成任何输出之前,step始终会计算其第二个参数,但是当foldr应用于无限列表时,它的第二个参数最终将取决于另一个步骤调用的结果

它不必以这种方式编写,但是你的第二个版本有点难看,因为它依赖于具有特定格式的步骤的初始参数,并且很难看出头/尾永远不会出错。 (我甚至不确定他们不会!)

您应该做的是重构第一个版本,以便在至少某些情况下产生输出而不依赖于输入列表。特别是我们可以看到,当字符不是空格时,输出列表中始终至少有一个元素。因此,延迟第二个参数的模式匹配,直到产生第一个元素。字符是空格的情况仍然依赖于列表,但这很好,因为案例可以无限递归的唯一方法是传入无限的空格列表,在这种情况下不产生任何输出并进入循环是单词的预期行为(它还能做什么?)

答案 2 :(得分:3)

第二个版本实际上不会评估result,直到之后开始生成自己的部分答案。第一个版本通过模式匹配立即评估result

这些无限列表的关键是你必须在开始要求列表元素之前生成某些东西,这样输出总是能够“保持领先”输入。

(我觉得这个解释不是很清楚,但这是我能做的最好的。)

答案 3 :(得分:1)

库函数foldr具有此实现(或类似):

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f k (x:xs) = f x (foldr f k xs)
foldr _ k _ = k

myWords_FailsOnInfiniteList的结果取决于foldr的结果,step的结果取决于foldr的结果,而myWords_FailsOnInfiniteList的结果取决于依赖于内部step的结果。等等无限列表,myWords_anotherReader会在产生第一个单词之前耗尽无限的空间和时间。

foldr中的tail ([...] ++ tail ([...] ++ tail (...)))函数在生成第一个单词的第一个字母之后才需要内部myWords :: String -> [String] myWords = myWords' . dropWhile isSpace where myWords' [] = [] myWords' string = let (part1, part2) = break isSpace string in part1 : myWords part2 的结果。不幸的是,正如Apocalisp所说,它在产生下一个单词之前使用O(第一个单词的长度)空间,因为正在生成第一个单词时,尾部thunk不断增长break :: (a -> Bool) -> [a] -> ([a], [a]) break p = span $ not . p span :: (a -> Bool) -> [a] -> ([a], [a]) span p xs = (takeWhile p xs, dropWhile p xs) takeWhile :: (a -> Bool) -> [a] -> [a] takeWhile p (x:xs) | p x = x : takeWhile p xs takeWhile _ _ = [] dropWhile :: (a -> Bool) -> [a] -> [a] dropWhile p (x:xs) | p x = dropWhile p xs dropWhile _ xs = xs


相比之下,与

相比
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"

使用可定义为

的库函数
words "" == []

请注意,生成中间结果永远不会被未来的计算所阻碍,只需要O(1)空间,因为结果的每个元素都可供使用。


附录

  

所以,这是修改后的代码。我通常试图避免头部和尾部,仅仅因为它们是部分功能,还因为我需要练习编写相应的模式匹配。

myWords "" = [""]

(旁白:您可能不在乎,但是图书馆中的myWords_anotherReader,但您的foldr。与尾随空格类似的问题。)

相对于\n -> tail $ myWords $ replicate n 'a' ++ " b" 进行了大幅改进,对于基于myWords_anotherReader的解决方案非常有用。

myWords

不可能比O(n)时间更好,但foldr\n -> head $ head $ myWords $ replicate n 'a' ++ " b" 都需要O(n)空间。考虑到myWords_anotherReader的使用,这可能是不可避免的。

更糟的是,

myWords

(x:xs)为O(1)但新myWords :: String -> [String] myWords = foldr step [""] . dropWhile isSpace where step space acc | isSpace space = "":acc step char ~(x:xs) = (char:x):xs 为O(n),因为模式匹配~需要进一步的结果。

您可以使用

解决此问题
{{1}}

{{1}}引入了“无可辩驳的模式”。无可辩驳的模式永远不会失败,也不会强制立即进行评估。