在 Haskell 中,是否有类似于 sub-guards 的东西? [英] Is there, in Haskell, something similar to sub-guards?

查看:28
本文介绍了在 Haskell 中,是否有类似于 sub-guards 的东西?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在编写一个关于音程分类的程序.概念结构相当复杂,我会尽可能清楚地表示它.前几行代码是一小段可以正常工作的摘录.第二个是满足我简洁性需求的伪代码.

I'm writing a program on the classification of musical intervals. The conceptual structure is quite complicated and I would represent it as clearly as possible. The first few lines of code are a small extract that works properly. The second are the pseudo-code that would meet my needs of conciseness.

interval pt1 pt2
  | gd == 0 && sd <  (-2) = ("unison",show (abs sd) ++ "d") 
  | gd == 0 && sd == (-2) = ("unison","dd")
  | gd == 0 && sd == (-1) = ("unison","d")
  | gd == 0 && sd == 0    = ("unison","P")
  | gd == 0 && sd == 1    = ("unison","A")
  | gd == 0 && sd == 2    = ("unison","AA")
  | gd == 0 && sd >  2    = ("unison",show sd ++ "A")

  | gd == 1 && sd <  (-1) = ("second",show (abs sd) ++ "d")
  | gd == 1 && sd == (-1) = ("second","dd")
  | gd == 1 && sd == 0    = ("second","d")
  | gd == 1 && sd == 1    = ("second","m")
  | gd == 1 && sd == 2    = ("second","M")
  | gd == 1 && sd == 3    = ("second","A")
  | gd == 1 && sd == 4    = ("second","AA")
  | gd == 1 && sd >  4    = ("second",show (abs sd) ++ "A")

  where
  (bn1,acc1,oct1) = parsePitch pt1
  (bn2,acc2,oct2) = parsePitch pt2
  direction = signum sd
  sd = displacementInSemitonesOfPitches pt1 pt2
  gd = abs $ displacementBetweenTwoBaseNotes direction bn1 bn2

是否有一种编程结构可以像下面的伪代码那样简化代码?

Is there a programming structure that could simplify the code like the following pseudo-code does?

interval pt1 pt2 
  | gd == 0  | sd <  (-2) = ("unison",show (abs sd) ++ "d") 
             | sd == (-2) = ("unison","dd")
             | sd == (-1) = ("unison","d")
             | sd == 0    = ("unison","P")
             | sd == 1    = ("unison","A")
             | sd == 2    = ("unison","AA")
             | sd >  2    = ("unison",show sd ++ "A")  
  | gd == 1  | sd <  (-1) = ("second",show (abs sd) ++ "d")
             | sd == (-1) = ("second","dd")
             | sd == 0    = ("second","d")
             | sd == 1    = ("second","m")
             | sd == 2    = ("second","M")
             | sd == 3    = ("second","A")
             | sd == 4    = ("second","AA")
             | sd >  4    = ("second",show (abs sd) ++ "A")
  | gd == 2  | sd ...     = ...
             | sd ...     = ...
  ...
  | mod gd 7 == 1 | mod sd 12 == ...
                  | mod sd 12 == ...
  ...
  | otherwise = ...

  where
  (bn1,acc1,oct1) = parsePitch pt1
  (bn2,acc2,oct2) = parsePitch pt2
  direction = signum sd
  sd = displacementInSemitonesOfPitches pt1 pt2
  gd = abs $ displacementBetweenTwoBaseNotes direction bn1 bn2

预先感谢您的建议.

推荐答案

让我用一个比发布的更短的例子:

Let me use a shorter example than the posted one:

original :: Int -> Int
original n
  | n < 10 && n > 7 = 1   -- matches 8,9
  | n < 12 && n > 5 = 2   -- matches 6,7,10,11
  | n < 12 && n > 3 = 3   -- matches 4,5
  | n < 13 && n > 0 = 4   -- matches 1,2,3,12

代码在 GHCi 中运行如下:

The code runs in GHCi as follows:

> map original [1..12]
[4,4,4,3,3,2,2,1,1,2,2,4]

我们的目标是将需要 n < 的两个分支组合"在一起.12,将这个条件分解出来.(这在 original 玩具示例中并不是一个巨大的进步,但在更复杂的情况下可能会如此.)

Our aim is to "group" together the two branches requiring with n < 12, factoring this condition out. (This is not a huge gain in the original toy example, but it could be in more complex cases.)

我们可以天真地考虑将代码拆分为两个嵌套的情况:

We could naively think of splitting the code in two nested cases:

wrong1 :: Int -> Int
wrong1 n = case () of 
  _ | n < 10 && n > 7 -> 1
    | n < 12 -> case () of
                _ | n > 5 -> 2
                  | n > 3 -> 3
    | n < 13 && n > 0 -> 4

或者,等效地,使用 MultiWayIf 扩展:

Or, equivalently, using the MultiWayIf extension:

wrong2 :: Int -> Int
wrong2 n = if 
  | n < 10 && n > 7 -> 1
  | n < 12 -> if | n > 5 -> 2
                 | n > 3 -> 3
  | n < 13 && n > 0 -> 4

然而,这会带来惊喜:

> map wrong1 [1..12]
*** Exception: Non-exhaustive patterns in case

> map wrong2 [1..12]
*** Exception: Non-exhaustive guards in multi-way if

问题是当 n1 时,n <12 分支被采用,内部情况被评估,然后没有分支考虑1.original 代码只是尝试处理它的下一个分支.但是,wrong1,wrong2 不会回溯到外壳.

The issue is that when n is 1, the n < 12 branch is taken, the inner case is evaluated, and then no branch there considers 1. The original code simply tries the next branch, which handles it. However, wrong1,wrong2 are not backtracking to the outer case.

请注意,当您知道外壳具有非重叠条件时,这不是问题.在 OP 发布的代码中,情况似乎如此,因此 wrong1,wrong2 方法可以在那里工作(如 Jefffrey 所示).

Please note that this is not a problem when you know that the outer case has non-overlapping conditions. In the code posted by the OP, this seems to be the case, so the wrong1,wrong2 approaches would work there (as shown by Jefffrey).

但是,一般情况下,可能存在重叠的情况呢?幸运的是,Haskell 是懒惰的,所以很容易滚动我们自己的控制结构.为此,我们可以如下利用 Maybe monad:

However, what about the general case, where there might be overlaps? Fortunately, Haskell is lazy, so it's easy to roll our own control structures. For this, we can exploit the Maybe monad as follows:

correct :: Int -> Int
correct n = fromJust $ msum 
   [ guard (n < 10 && n > 7) >> return 1
   , guard (n < 12)          >> msum
      [ guard (n > 5) >> return 2
      , guard (n > 3) >> return 3 ]
   , guard (n < 13 && n > 0) >> return 4 ]

有点冗长,但不多.以这种风格编写代码比看起来容易:一个简单的多路条件写为

It is a bit more verbose, but not by much. Writing code in this style is easier than it might look: a simple multiway conditional is written as

foo n = fromJust $ msum 
   [ guard boolean1 >> return value1
   , guard boolean2 >> return value2
   , ...
   ]

而且,如果你想要一个嵌套"的情况,只需用 msum [ ... ] 替换任何返回值.

and, if you want a "nested" case, just replace any of the return value with a msum [ ... ].

这样做可以确保我们得到想要的回溯.确实:

Doing this ensures that we get the wanted backtracking. Indeed:

> map correct [1..12]
[4,4,4,3,3,2,2,1,1,2,2,4]

这里的技巧是当 guard 失败时,它会生成一个 Nothing 值.库函数msum 只是选择列表中的第一个非Nothing 值.因此,即使内部列表中的每个元素都是 Nothing,外部 msum 也会考虑外部列表中的下一项——根据需要进行回溯.

The trick here is that when a guard fails, it generates a Nothing value. The library function msum simply selects the first non-Nothing value in the list. So, even if every element in the inner list is Nothing, the outer msum will consider the next item in the outer list -- backtracking, as wanted.

这篇关于在 Haskell 中,是否有类似于 sub-guards 的东西?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆