我已经习惯了Haskell的高阶函数。通常我可以使用map,fold和scan等函数替换显式的递归模式。但是,我经常遇到以下递归模式,我不明白如何使用高阶函数表达:
f (x:[]) = k x
f (x:xs) = g x (f xs)
例如,假设我代表分析表。然后我创建一个数据类型,如:
data Tableau = N Expr | S Expr (Tableau) | B Expr (Tableau) (Tableau)
如果我想将Expr
的列表转换为tableau结构,我想要一个函数部分可能类似于:
f (x:[]) = N x
f (x:xs) = S x (f xs)
现在,我看到三个选项:(1)创建一个函数,在给定一个画面和一个列表的情况下,决定画面中的下一个分支是S
还是N
(或{ {1}},但我们会忽略这种情况); (2)使用高阶函数来封装B
的递归模式; (3)使用像f
这样的函数。
最好的选择是什么?
答案 0 :(得分:8)
我最有可能使用以下内容:
f xs = foldr g (k (last xs)) (init xs)
它基本上意味着折叠时列表的末尾被k x
替换。感谢各地的懒惰评估,它甚至适用于无限列表。
还有另外两种解决方案 - 添加空案例并使用Maybe。
A)添加空案例:
如果f []
定义明确,那将是最好的。然后,您可以将定义写为
f [] = c
f (x:xs) = g x (f xs)
是f = foldr g c
。例如,如果您更改
data Tableau = N Expr | S Expr Tableau | B Expr Tableau Tableau
到
data Tableau = N | S Expr Tableau | B Expr Tableau Tableau
然后你可以将单元素表格表示为S expr N
,并且该函数被定义为单行
f = foldr S N
只要空案合理,这是最好的解决方案。
B)使用Maybe:
另一方面,如果f []
无法明确定义,则情况会更糟。
部分功能通常被认为是丑陋的。要使其完整,您可以使用Maybe
。定义
f [] = Nothing
f [x] = Just (k x)
f (x:xs) = Just (g x w)
where Just w = f xs
这是一个完整的功能 - 更好。
但是现在你可以将函数重写为:
f [] = Nothing
f (x:xs) = case f xs of
Nothing -> Just (k x)
Just w -> Just (g x w)
这是一个正确的折叠:
addElement :: Expr -> Maybe Tableaux -> Maybe Tableaux
addElement x Nothing = Just (N x)
addElement x (Just w) = Just (S x w)
f = foldr addElement Nothing
通常,折叠是惯用的,应该在适合递归模式时使用。否则使用显式递归或尝试重用现有的组合器。如果有一个新的模式,做一个组合,但只有你会使用该模式 - 否则它是过度的。在这种情况下,模式是由非data List a = End a | Cons a (List a)
定义的非空列表折叠。
答案 1 :(得分:4)
如果我已正确理解了这个问题,那么这是我对你的选择的评价:
为了编写该函数,必须从构造函数下面匹配(可能是任意复杂的?)画面可能有点讨厌。这种做法似乎有点脆弱,虽然它可能会运作得很好。
我认为没有必要概括出模式,因为它是一个在递归结构上运行的递归函数。引入更高阶模式(我认为)会混淆执行此数据结构的递归遍历背后的实际逻辑。
我认为这很有道理。正如你所观察到的那样,它是一种合理认可的“模式”,但我认为它与算法的描述相匹配,以这种方式将其写下来。它可能不是一般的或可重用的,但鉴于它本质上是算法方法的关键,我认为直接编写案例是有道理的,就像你在f这样的函数中所做的那样。这将是我最喜欢的方法。
很抱歉没有提供一个特别具体的答案,但这是一个相当主观的答案,所以考虑到上面的三个选项,我会选择选项3,原因是清晰度和可读性。