关于Haskell中的控制流结构(多个if-then-else)

关于Haskell中的控制流结构(多个if-then-else),haskell,if-statement,functional-programming,Haskell,If Statement,Functional Programming,我想将以下过程程序翻译成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

我想将以下过程程序翻译成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 
不幸的是,它包含(或者dx)三次

是否有更好的方法来实现该功能?(即,如果不满足任何条件,则返回(dx))


我知道我们可以把条件c1和c2组合成(c1x)&(c2x)来减少if的数量,但是我的条件c1,c2,c3,c4确实很长,如果我把它们合并起来,我会得到一个需要不止一行的条件。

如果你只是担心把它们写出来,那就是
块的作用

f x = 
  case () of
    () | c1 && c2       -> a x
       | c1 && c3 && c4 -> b x
       | otherwise      -> d x
  where
    c1 = ...
    c2 = ...
    c3 = ...
    c4 = ...

并不是说我在使用
case
技巧来为guard语句引入一个新的位置。我不能在函数定义本身上使用保护,因为
where
子句不能覆盖所有的保护。如果
相同,您可以使用
,但guards具有良好的传递语义。

您可以使用guards和
where
子句:

f x | cb && c2 x = a x
    | cb && c3 x && c4 x = b x
    | otherwise = d x
  where cb = c1 x
最简单、最明显的解决方案 如果您正在使用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)]
+-> c1 +-> c2 -> a
|      |
|      +-> 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
这是通过遍历需求列表并找到满足的第一组决策(
maybeMatch
)。它从
Maybe
(您可能需要一些更好的错误处理!)中提取出来,然后选择相应的函数(
result
),然后运行
x


非常先进和灵活的解决方案 如果您有一个非常复杂的决策树,您可能不想用一个简单的列表来表示它。这就是实际数据树派上用场的地方。您可以创建所需函数的树,然后搜索该树,直到找到叶节点。在本例中,该树可能类似于

decisions :: [([a -> Bool], a -> b)]

decisions = [([c1, c2],     a)
            ,([c1, c3, c4], b)]
            ,([],           d)]
+-> c1 +-> c2 -> a
|      |
|      +-> c3 -> c4 -> b
+-> d
换句话说,如果
x
满足
c1
,它将查看它是否也满足
c2
,以及它是否对
x
采取了
a
行动。如果没有,它将使用
c3
继续执行下一个分支,以此类推,直到到达一个动作(或遍历整个树)

但首先需要一个数据类型来区分需求(
c1
c2
等)和操作(
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
这使用了一点魔法(
asum
)在第一次成功命中时停止。这反过来意味着它不会徒劳地计算任何分支的条件(如果计算很昂贵,这是有效且重要的),并且它应该能够很好地处理无限决策树

您可以充分利用
备选方案
类,使
Decision
更加通用,但我选择将其专门用于
也许
,以便不写关于此的书。让它更一般可能会让你有花哨的一元决策也,这将是非常酷的

但是,最后,作为一个非常简单的例子,这一点正在发挥作用。如果你给我一个数字,并问我下一个数字应该是什么,我可以建立一个决策树来找出答案。该树可能如下所示:

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
你可以想象出更有趣的决策树


一年后编辑:对替代方案的概括实际上非常简单,而且相当有趣。
decise
函数获得了新的外观

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)
要求下进行,而没有其他要求。这在技术上是不正确的,但是如果我们只得到第一个结果(这基本上就是使用
可能
替代实例所做的事情),那么它正好起作用。如果我们列出所有的结果,我们也会得到一个不正确的结果

什么时候获得多个结果有趣?也许我们正在为人工智能编写决策树,我们希望通过首先获得