我想将以下程序程序翻译成Haskell [用伪代码编写]:
f(x) {
if(c1(x)) {
if(c2(x)) {
return a(x);
}
else if (c3(x)) {
if(c4(x)) {
return b(x);
}
}
return d(x);
}
我写了以下实现:
f x =
if (c1 x) then
if(c2 x) then
a x
else if (c3 x) then
if (c4 x) then
b x
else d x
else d x
else d x
不幸的是它包含(否则为d x)三次。
有没有更好的方法来实现这个功能? (即,如果没有满足任何条件,则返回(d x)?)
据我所知,我们可以将条件c1和c2组合成(c1 x)&& (c2 x)使if的数量变小,但我的条件c1,c2,c3,c4确实很长,如果我将它们组合起来,我将得到一个多于一行的条件。
答案 0 :(得分:37)
如果你正在使用GHC,你可以打开
{-# LANGUAGE MultiWayIf #-}
你的整个事情变成了
f x = if | c1 x && c2 x -> a x
| c1 x && c3 x && c4 x -> b x
| otherwise -> d x
但是,并不总是想要在Haskell中盲目地复制命令式代码。通常,将代码视为数据非常有用。您真正要做的是设置x
必须满足的要求列表,然后如果x
满足这些要求,您就会对x
采取一些措施。
我们可以用Haskell中的实际函数列表来表示它。它看起来像
decisions :: [([a -> Bool], a -> b)]
decisions = [([c1, c2], a)
,([c1, c3, c4], b)]
,([], d)]
在此,我们应将其视为“如果x
同时满足c1
和c2
,则对a
”采取行动x
,依此类推。然后我们可以将f
定义为
f x = let maybeMatch = find (all ($ x) . fst) decisions
match = fromMaybe (error "no match!") maybeMatch
result = snd match
in result x
这是通过遍历需求列表并找到x
满足(maybeMatch
)的第一组决策来实现的。它从Maybe
中提取出来(你可能希望在那里有更好的错误处理!)然后它选择相应的函数(result
),然后通过它运行x
。
如果您有一个非常复杂的决策树,您可能不希望用平面列表来表示它。这是实际数据树派上用场的地方。您可以创建所需函数的树,然后搜索该树,直到您点击叶节点。该示例中的树可能类似于
+-> c1 +-> c2 -> a
| |
| +-> c3 -> c4 -> b
+-> d
换句话说,如果x
满足c1
,它会看到它是否也满足c2
,如果它确实在a
上采取了行动x
。如果没有,则继续使用c3
进入下一个分支,依此类推,直到它到达某个动作(或已经遍历整个树)。
但首先,您需要一种数据类型来区分需求(c1
,c2
等)和操作(a
,{{1}等等。)
b
然后你建立一个决策树
data Decision a b = Requirement (a -> Bool)
| Action (a -> b)
这看起来比它复杂,所以你应该发明一种表达决策树的更简洁的方法。如果定义函数
decisions =
Node (Requirement (const True))
[Node (Requirement c1)
[Node (Requirement c2)
[Node (Action a) []]
,Node (Requirement c3)
[Node (Requirement c4)
[Node (Action b) []]]
,Node (Action d) []]
你可以把树写成
iff = Node . Requirement
action = flip Node [] . Action
突然它与您开始使用的命令式代码非常相似,尽管它是有效的Haskell代码,它只是构建数据结构! Haskell非常适合在这种语言中定义自定义的“语言”。
然后,您需要在树中搜索您可以达到的第一个操作。
decisions =
iff (const True) [
iff (c1) [
iff (c2) [
action a
],
iff (c3) [
iff (c4) [
action b
]
]
],
action d
]
这会使用一点Maybe Maybe(decide :: a -> Tree (Decision a b) -> Maybe b
decide x (Node (Action f) _) = Just (f x)
decide x (Node (Requirement p) subtree)
| p x = asum $ map (decide x) subtree
| otherwise = Nothing
)来阻止第一次成功击中。这反过来意味着它不会徒劳地计算任何分支的条件(如果计算成本很高,这是有效且重要的)并且它应该处理无限的决策树。
你可以使asum
更加通用,充分利用decide
课程,但我选择将其专门用于Alternative
,以便不写一本关于此的书。让它变得更加通用可能会让你有一个奇特的monadic决定,这将非常酷!
但是,最后,作为一个非常简单的例子 - 采取Collatz conjecture。如果你给我一个号码,并问我下一个号码应该是什么,我可以建立一个决策树来找出答案。树可能看起来像这样:
Maybe
所以数字必须大于0,然后如果它是奇数你加3并加1,否则你将它减半。测试运行显示
collatz =
iff (> 0) [
iff (not . even) [
action (\n -> 3*n + 1)
],
action (`div` 2)
]
您可以想象更多有趣的决策树。
编辑就像一年之后:对Alternative的概括实际上非常简单,而且非常有趣。 λ> decide 3 collatz
Just 10
λ> decide 10 collatz
Just 5
λ> decide (-4) collatz
Nothing
函数获得新外观
decide
(对于那些保持计数的人来说,这总共只有三个变化。)这给你的机会是通过使用列表的应用实例而不是Maybe来组合输入所满足的“所有”动作。这会在我们的decide :: Alternative f => a -> Tree (Decision a b) -> f b
decide x (Node (Action f) _) = pure (f x)
decide x (Node (Requirement p) subtree)
| p x = asum $ map (decide x) subtree
| otherwise = empty
树中显示“错误” - 如果我们仔细查看它,我们会看到所有奇数和正整数collatz
转向{{1} } 但它还表示所有正数都转为n
。没有额外要求说数字必须是均匀的。
换句话说,3*n +1
操作仅在<{1}}要求下 ,而不是其他内容。这在技术上是不正确的,但是如果我们得到第一个结果(这基本上是使用n/2
替代实例的那个),它就会起作用。如果我们列出所有结果,我们也会得到一个不正确的结果。
什么时候获得多个结果有趣?也许我们正在为AI编写决策树,我们希望通过首先获得所有有效决策,然后随机选择其中一个来使行为人性化。或者根据他们在这种情况下的优势或其他方面对他们进行排名。
答案 1 :(得分:18)
您可以使用警卫和where
子句:
f x | cb && c2 x = a x
| cb && c3 x && c4 x = b x
| otherwise = d x
where cb = c1 x
答案 2 :(得分:16)
如果您只是担心将它们写出来那么where
块就是
f x =
case () of
() | c1 && c2 -> a x
| c1 && c3 && c4 -> b x
| otherwise -> d x
where
c1 = ...
c2 = ...
c3 = ...
c4 = ...
并非我使用case
技巧为守卫声明引入新地方。我不能在函数定义本身使用保护,因为where
子句不会覆盖所有守卫。你可以使用if
,但守卫有很好的传递语义。
答案 3 :(得分:8)
你可以使用另一种模式:我不会在你的具体例子中使用它,但是在我使用它的情况非常相似。
f x = case (c1 x, c2 x, c3 x, c4 x) of
(True,True,_,_) -> a x
(True,False,True,True) -> b x
_ -> d x
实际上只评估选择要采用的路径所需的最低评估:除非c2 x
为c1 x
,否则实际上不会评估True
。