3

DRY 原則を適用する特定の方法が Haskell の良い実践と見なされるかどうかについて質問があります。例を示してから、私が取っているアプローチが良い Haskell スタイルと見なされるかどうかを尋ねます。簡単に言えば、質問は次のとおりです。長い数式があり、その数式の小さなサブセットを別の場所で繰り返す必要がある場合、DRY を維持できるように数式の繰り返しサブセットを常に変数に入れますか? ? なぜですか、そうでないのですか?

例: 数字の文字列を取得し、その文字列を対応する Int 値に変換するとします。(ところで、これは「Real World Haskell」の演習です)。

エッジケースを無視することを除いて機能するソリューションは次のとおりです。

asInt_fold string = fst (foldr helper (0,0) string)
  where
    helper char (sum,place) = (newValue, newPlace)
      where 
        newValue = (10 ^ place) * (digitToInt char) + sum
        newPlace = place + 1

これは foldr を使用し、アキュムレータは次の桁の値とこれまでの合計のタプルです。

ここまでは順調ですね。ここで、特殊なケースのチェックを実装しようとしたときに、エラーをチェックするためにさまざまな場所で "newValue" 式の小さな部分が必要であることがわかりました。たとえば、私のマシンでは、入力が (2^31 - 1) より大きい場合に Int オーバーフローが発生するため、処理できる最大値は 2,147,483,647 です。したがって、私は2つのチェックを入れました:

  1. 位の値が 9 (10 億の位) で、桁の値が > 2 の場合、エラーがあります。
  2. sum + (10 ^ place) * (digitToInt char) > maxInt の場合、エラーが発生します。

これらの 2 つのチェックにより、式の一部を繰り返す必要が生じたため、次の新しい変数を導入しました。

  • digitValue = digitToInt char
  • newPlaceComponent = (10^place) * digitValue

これらの変数を導入した理由は、単に DRY 原則を自動的に適用するためです。数式のこれらの部分を繰り返すことに気付いたので、それらを 1 回だけ定義しました。

しかし、これは良い Haskell スタイルと見なされるのでしょうか。明らかな利点がありますが、欠点も見られます。コードは間違いなく長くなりますが、私が見た Haskell コードの多くはかなり簡潔です。

では、あなたはこの Haskell スタイルが良いと考えていますか? また、この慣行に従っていますか? なぜ/なぜではないのですか?

価値があるのは、多くのエッジケースを処理するため、非常に大きな where ブロックを持つ私の最終的なソリューションです。DRY 原則を適用した結果、ブロックがどれだけ大きくなったかがわかります。

ありがとう。

asInt_fold "" = error "You can't be giving me an empty string now"
asInt_fold "-" = error "I need a little more than just a dash"
asInt_fold string | isInfixOf "." string = error "I can't handle decimal points"
asInt_fold ('-':xs) = -1 * (asInt_fold xs) 
asInt_fold string = fst (foldr helper (0,0) string)
  where
    helper char (sum,place) | place == 9 && digitValue > 2 = throwMaxIntError
               | maxInt - sum < newPlaceComponent      = throwMaxIntError
                   | otherwise                             = (newValue, newPlace)
            where
              digitValue =  (digitToInt char)
              placeMultiplier = (10 ^ place)
              newPlaceComponent = placeMultiplier * digitValue
              newValue = newPlaceComponent + sum
              newPlace = place + 1
              maxInt = 2147483647
              throwMaxIntError = 
                        error "The value is larger than max, which is 2147483647"
4

3 に答える 3

9

DRY は、他の場所と同様に Haskell の原則として優れています :) Haskell であなたが話す簡潔さの背後にある多くの理由は、多くのイディオムがライブラリに持ち出されていることです。それらを簡潔にするために非常に慎重に検討しました:)

たとえば、数字から文字列へのアルゴリズムを実装する別の方法を次に示します。

asInt_fold ('-':n) = negate (asInt_fold n)
asInt_fold "" = error "Need some actual digits!"
asInt_fold str = foldl' step 0 str
    where
        step _ x
            | x < '0' || x > '9'
            = error "Bad character somewhere!"
        step sum dig =
            case sum * 10 + digitToInt dig of
                n | n < 0 -> error "Overflow!"
                n -> n

注意すべき点がいくつかあります。

  1. オーバーフローが発生したときにそれを検出します。許可する桁数に恣意的な制限を設定するのではありません。これにより、オーバーフロー検出ロジックが大幅に簡素化され、Int8 から Integer までの任意の整数型で機能します [オーバーフローがラップアラウンドになるか、発生しないか、または加算演算子自体からのアサーションが発生する限り]
  2. 別のフォールドを使用することで、2 つの別々の状態は必要ありません。
  3. わざわざ物事を取り上げなくても、同じことを繰り返す必要はありません。言いたいことを言い直すことは自然にできなくなります。

さて、アルゴリズムを書き換えて重複をなくすことが常に可能であるとは限りませんが、一歩下がって、問題についてどのように考えてきたかを再考することは常に役に立ちます:)

于 2009-05-06T05:10:13.943 に答える
2

あなたのやり方は理にかなっていると思います。

繰り返し計算を回避することが重要な場合は、繰り返し計算を個別に定義された値に分割する必要がありますが、この場合は必要ないようです。それでも、分割された値にはわかりやすい名前が付いているため、コードを理解しやすくなります。結果としてコードが少し長くなるという事実は悪いことではないと思います。

ところで、最大 Int をハードコーディングする代わりに、 (maxBound :: Int) を使用できます。これにより、間違いを犯したり、別の最大 Int を使用した別の実装でコードが壊れたりするリスクを回避できます。

于 2009-05-06T05:02:26.647 に答える