13

私は音程の分類に関するプログラムを書いています。概念的な構造は非常に複雑で、できるだけ明確に表現したいと思います。コードの最初の数行は、適切に機能する小さな抜粋です。2 つ目は、簡潔にするという私のニーズを満たす疑似コードです。

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 を必要とする 2 つの分岐を「グループ化」してn < 12、この条件を除外することです。(これは、originalおもちゃの例では大きな利点ではありませんが、より複雑なケースでは大きくなる可能性があります。)

コードを 2 つのネストされたケースに分割することを単純に考えることができます。

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

問題は、nis1の場合、n < 12ブランチが取得され、内側のケースが評価され、その後、ブランチが考慮されないこと1です。コードはoriginal、それを処理する次の分岐を試行するだけです。ただし、wrong1,wrong2アウターケースには後戻りしていません。

これは、外側のケースに重複しない条件があることがわかっている場合は問題ありません。OPによって投稿されたコードでは、これが事実であるように思われるため、wrong1,wrong2アプローチはそこで機能します(Jefffreyが示すように)。

しかし、重複する可能性がある一般的なケースはどうでしょうか? 幸いなことに、Haskell は遅延型であるため、独自の制御構造を展開するのは簡単です。このために、Maybe次のようにモナドを利用できます。

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値を選択するだけです。そのため、内側のリストのすべての要素が であってもNothingmsum外側のリストは外側のリストの次の項目を考慮します。必要に応じてバックトラックします。

于 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 に答える