正如 Gabriel 在评论中所说,您正在以一种非常不寻常的方式将手动递归(模式匹配(x:xs)
)与foldr
。通常您想使用手动递归,或者foldr
在递归遵循“重复将函数应用于列表的元素直到用尽列表”的模式的情况下使用。
我假设你的add
函数看起来像这样:
add :: String -> BST -> BST
add string Empty = MakeNode Empty string Empty
add string (MakeNode l s r) =
if string < s
then MakeNode (add string l) s r
else MakeNode l s (add string r)
有了这个,该函数listToTree
通常会以两种方式之一编写。首先是使用模式匹配和递归:
listToTree [] = Empty
listToTree (x:xs) = add x (listToTree xs)
也就是说,要么你有一个空列表,在这种情况下你返回空树,或者你有一个头后跟一个尾,在这种情况下,你将列表的头部添加到列表尾部返回的树中。
另一种方法是listToTree
通过折叠列表来编写。这为您抽象出递归,这样您就可以编写
listToTree = foldr add Empty
这是有效的,因为foldr
有类型
foldr :: (a -> b -> b) -> b -> [a] -> b
并且add
有Empty
类型
add :: String -> BST -> BST
Empty :: BST
专门化类型a
和b
,你得到
foldr :: (String -> BST -> BST) -> BST -> [String] -> BST
意思就是
foldr add Empty :: [String] -> BST
您应该更喜欢哪一个?也许第一个对于初学者来说更容易阅读和理解。然而,随着您获得更多使用 Haskell 的经验,您会发现第二个版本变得更容易理解。它也更简洁,而且它是以折叠的形式编写的这一事实允许更频繁地触发列表融合规则,这可能会产生更高效的代码。
了解文件夹
在我看来,理解折叠的关键是要意识到折叠会用你给它的任何函数和常量替换列表构造函数。在 Haskell 中,列表有两种可能的构造函数:
[] :: [a]
(:) :: a -> [a] -> [a]
当您对所有语法进行脱糖处理时,列表实际上看起来像这样(这是有效的 Haskell - 试试看!)
xs = 1 : 2 : 3 : []
当您调用foldr op x0 xs
时,折叠有效地将所有(:)
构造函数xs
替换为op
,并将所有[]
构造函数替换为x0
:
foldr op x0 xs = 1 `op` 2 `op` 3 `op` x0
当然,这里有一个歧义,因为我们不知道是op
左联还是右联。为了使类型起作用,您必须提供一个与右侧关联的函数(这就是它被称为右折叠的原因),如下所示:
foldr op x0 xs = 1 `op` (2 `op (3 `op` x0))
左折叠是相同的,除了它关联到左侧(并将初始值放在列表的开头而不是结尾)所以你最终得到
foldl op x0 xs = ((x0 `op` 1) `op` 2) `op` 3