【问题标题】:On control flow structures in Haskell (multiple if-then-else)Haskell 中的控制流结构(多个 if-then-else)
【发布时间】:2014-06-23 16:52:00
【问题描述】:

我想将以下程序程序翻译成 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 

不幸的是,它包含 (else d x) 三次。

有没有更好的方法来实现这个功能? (即,如果不满足任何条件,则返回 (d x)?)

我知道我们可以将条件 c1 和 c2 组合成 (c1 x) && (c2 x) 以使 if 的数量更小,但是我的条件 c1、c2、c3、c4 确实很长,如果我将它们组合起来我会得到一个需要多行的条件。

【问题讨论】:

  • 这属于 codereview.stackexchange

标签: haskell if-statement functional-programming


【解决方案1】:

最简单、最明显的解决方案

如果你使用 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 同时满足c1c2,则对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 继续到下一个分支,依此类推,直到它到达一个动作(或遍历整个树)。

但首先您需要一种数据类型来区分需求(c1c2 等)和操作(ab 等)

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 编写决策树,并且我们希望通过首先获取所有有效决策,然后随机选择其中一个来使行为人性化。或者根据他们在环境中的表现或其他因素对他们进行排名。

【讨论】:

  • 谢谢!这绝对比我预期从这个问题中学到的要多。
  • 最简单的解决方案的更便携,如果有点hacky,版本:使用case 声明和警卫。也就是说:case () of _ | c1 x && c2 x -> a x ; | c1 x && c3 x && c4 x -> b x ; | otherwise -> d x。不过,在使用足够新的 GHC 时,我更喜欢MultiWayIf;但在旧版本上,你不能,这就是它的模拟方式。
  • @AntalS-Z 绝对!不过,其他答案已经涵盖了这一点,因此我决定也不将其添加到我的答案中,以保持简短。我的希望是,当未来的 Google 人到来时,MultiWayIf 不会太新。 The GHC documentation states the equivalence 也有兴趣的人。
【解决方案2】:

您可以使用另一种模式:我不会在您的具体示例中使用它,但在非常相似的情况下我使用过它。

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 xTrue

【讨论】:

    【解决方案3】:

    您可以使用守卫和where 子句:

    f x | cb && c2 x = a x
        | cb && c3 x && c4 x = b x
        | otherwise = d x
      where cb = c1 x
    

    【讨论】:

    • 我会怎么做;简短、结构化且易于阅读
    【解决方案4】:

    如果您只是担心将它们写出来,那么这就是 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,但是守卫有很好的传递语义。

    【讨论】:

    • 我使用的不应该是Note吗...?
    • where 子句 do 作用于多个警卫。这只是它们没有覆盖的多个参数模式。
    猜你喜欢
    • 1970-01-01
    • 2012-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-19
    • 1970-01-01
    相关资源
    最近更新 更多