最简单、最明显的解决方案
如果你使用 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,则对x 采取行动a”等等。那么我们可以将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、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
你可以把树写成
decisions =
iff (const True) [
iff (c1) [
iff (c2) [
action a
],
iff (c3) [
iff (c4) [
action b
]
]
],
action d
]
突然之间,它与您开始使用的命令式代码非常相似,尽管它只是构建数据结构的有效 Haskell 代码! Haskell 非常强大,可以像这样定义自定义的小“语言中的语言”。
然后您需要在树中搜索您可以到达的第一个操作。
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
这使用了一点 Maybe 魔法 (asum) 在第一次成功命中时停止。这反过来意味着它不会徒劳地计算任何分支的条件(如果计算成本很高,这将是高效且重要的)并且它应该可以很好地处理无限决策树。
您可以使decide 更加通用,充分利用Alternative 类,但我选择专门为Maybe 编写它,以免写关于这方面的书。让它更通用可能会让你也有花哨的一元决策,这将是非常酷的!
但是,最后,作为一个非常简单的例子——以Collatz conjecture为例。如果你给我一个数字,然后问我下一个数字应该是多少,我可以建立一个决策树来找出答案。树可能如下所示:
collatz =
iff (> 0) [
iff (not . even) [
action (\n -> 3*n + 1)
],
action (`div` 2)
]
所以这个数字必须大于 0,然后如果它是奇数,则乘以 3 并加 1,否则将其减半。测试运行表明
λ> decide 3 collatz
Just 10
λ> decide 10 collatz
Just 5
λ> decide (-4) collatz
Nothing
您可能可以想象出更有趣的决策树。
一年后编辑:对 Alternative 的概括实际上非常简单,而且相当有趣。 decide 函数焕然一新
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
(对于那些保持计数的人来说,总共只有三个更改。)这为您提供了通过使用列表的应用实例而不是 Maybe 来组装输入满足的“所有”操作的机会。这揭示了我们的collatz 树中的一个“错误”——如果我们仔细观察它,我们会发现它表示所有奇数 和 正整数 n 都变成了 3*n +1 但是 它还说所有正数都转向n/2。没有额外的要求说数字必须是偶数。
换句话说,(`div` 2) 操作仅符合(>0) 要求,仅此而已。这在技术上是不正确的,但如果我们只得到第一个结果(这基本上是使用 Maybe Alternative 实例所做的),它就会起作用。如果我们列出所有结果,我们也会得到一个不正确的结果。
什么时候获得多个结果很有趣?也许我们正在为 AI 编写决策树,并且我们希望通过首先获取所有有效决策,然后随机选择其中一个来使行为人性化。或者根据他们在环境中的表现或其他因素对他们进行排名。