13

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

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

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

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

预先感谢您的建议。

4

3 回答 3

8

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

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 中运行如下:

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

我们的目标是将需要 with 的两个分支“组合”在一起,将n < 12这个条件排除在外。(这在玩具示例中并不是一个巨大的收获original,但在更复杂的情况下可能是这样。)

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

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扩展名:

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

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

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

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

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 ]

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

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

并且,如果您想要“嵌套”案例,只需将其中的任何一个替换return valuemsum [ ... ].

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

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

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

于 2015-02-15T18:25:01.087 回答
7

我建议将每个嵌套条件分组到一个函数中:

interval :: _ -> _ -> (String, String)
interval pt1 pt2
    | gd == 0 = doSomethingA pt1 pt2
    | gd == 1 = doSomethingB pt1 pt2
    | gd == 2 = doSomethingC pt1 pt2
    ...

然后,例如:

doSomethingA :: _ -> _ -> (String, String)
doSomethingA pt1 pt2
    | 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")
    where sd = displacementInSemitonesOfPitches pt1 pt2  

或者,您可以使用MultiWayIf语言扩展:

interval pt1 pt2 =
    if | gd == 0 -> if | sd <  (-2) -> ("unison",show (abs sd) ++ "d") 
                       | sd == (-2) -> ("unison","dd")
                       | sd == (-1) -> ("unison","d")
                       ...
       | gd == 1 -> if | sd <  (-1) -> ("second",show (abs sd) ++ "d")
                       | sd == (-1) -> ("second","dd")
                       | sd == 0    -> ("second","d")
                       ...
于 2015-02-15T14:00:43.647 回答
4

这并不是对标题问题的真正答案,而是针对您的特定应用程序。类似的方法将适用于您可能希望使用此类子守卫的许多其他问题。

首先,我建议您开始少用“字符串类型”:

interval' :: PitchSpec -> PitchSpec -> Interval

data Interval = Unison PureQuality
              | Second IntvQuality
              | Third IntvQuality
              | Fourth PureQuality
              | ...

data IntvQuality = Major | Minor | OtherQual IntvDistortion
type PureQuality = Maybe IntvDistortion
data IntvDistortion = Augm Int | Dimin Int   -- should actually be Nat rather than Int

无论如何,您的特定任务可以通过“计算”值来更优雅地完成,而不是与一堆硬编码的案例进行比较。基本上,您需要的是:

type RDegDiatonic = Int
type RDeg12edo = Rational  -- we need quarter-tones for neutral thirds etc., which aren't in 12-edo tuning

courseInterval :: RDegDiatonic -> (Interval, RDeg12edo)
courseInterval 0 = ( Unison undefined, 0   )
courseInterval 1 = ( Second undefined, 1.5 )
courseInterval 2 = ( Third undefined,  3.5 )
courseInterval 3 = ( Fourth undefined, 5   )
...

然后,您可以通过将 12edo 大小与您给定的大小进行比较来“填充”那些未定义的间隔质量,使用1

class IntervalQuality q where
  qualityFrom12edoDiff :: RDeg12edo -> q

instance IntervalQuality PureQuality where
  qualityFrom12edoDiff n = case round n of
         0 -> Nothing
         n' | n'>0       -> Augm n
            | otherwise  -> Dimin n'
instance IntervalQuality IntvQuality where
  qualityFrom12edoDiff n | n > 1      = OtherQual . Augm $ floor n
                         | n < -1     = OtherQual . Dimin $ ceil n
                         | n > 0      = Major
                         | otherwise  = Minor

有了它,您可以实现您的功能:

interval pt1 pt2 = case gd of
       0 -> Unison . qualityFrom12edoDiff $ sd - 0
       1 -> Second . qualityFrom12edoDiff $ sd - 1.5
       2 -> Third  . qualityFrom12edoDiff $ sd - 3.5
       3 -> Fourth . qualityFrom12edoDiff $ sd - 5
       ...


1你在这里并不需要一个类型类,我也可以为纯区间和其他区间定义两个不同命名的函数。

于 2015-02-16T10:49:48.457 回答