マネーモノイド







Mark Seemanが関数型プログラミングについてすばやく簡単に語っています。 これを行うために、彼はデザインパターンとカテゴリ理論の関係に関する一連の記事を書き始めました。 15分の空き時間があるOOPshnikは、機能だけでなく、適切なオブジェクト指向の設計に関する根本的に新しいアイデアと洞察のセットを手に入れることができます。 決定的な要因は、 すべての例が実際のC#、F#、およびHaskellコードであることです。



このハブラポストは、モノイドに関する一連の記事の2番目の記事です。





始める前に、記事のタイトルについて少し話をしたいと思います。 2003年、Kent Beckの著書、 Extreme Programming:Development through Testingは 、元々例としてTest-Driven Developmentと呼ばれていましたが、すでにベストセラーになっています。 そのような「例」の1つは「お金の例」でした。これは、10ドルや10フランの追加など、多通貨操作を実行できるアプリケーションを作成およびリファクタリングする例です。 この記事のタイトルはこの本への参照であり、記事の内容をよりよく理解するために、その最初の部分をよく理解することを強くお勧めします。



「お金の例」Kent Beckには興味深い特性がいくつかあります。



要するに、モノイドは、中立的な要素( ユニティとも呼ばれることもある)を持つ連想バイナリ操作です。



彼の本の最初の部分で、Kent Beckは「テストによる開発」の原則を使用して、シンプルで柔軟な「マネーAPI」を作成する可能性を探ります。 その結果、彼は解決策を得ましたが、その設計にはさらに手の込んだ作業が必要です。



ケントベックAPI



この記事では、 Jawar AminによってC# (元のコードはJavaで作成された)に翻訳されたKent Beckの本のコードを使用します。



ケント・ベックは本の中で、「5 USD + 10 CHF」などの表現を扱うことができる、いくつかの通貨でお金を処理できるオブジェクト指向APIを開発していました。 最初のパートの終わりに向かって、彼は(C#に翻訳された)次のようなインターフェースを作成します。



public interface IExpression { Money Reduce(Bank bank, string to); IExpression Plus(IExpression addend); IExpression Times(int multiplier); }
      
      





Reduce



メソッドは、 IExpression



オブジェクトをMoney



オブジェクトとして表される特定の通貨( to



パラメーター)に変換します。 これは、複数の通貨を含む式がある場合に便利です。



Plus



メソッドは、 IExpression



オブジェクトを現在のIExpression



オブジェクトに追加し、新しいIExpression



を返します。 1つの通貨または複数の通貨でお金を支払うことができます。



Times



メソッドは、 IExpression



に特定の係数を乗算します。 おそらく、すべての例で、因子と合計に整数を使用していることに気づいたでしょう。 Kent Beckはコードを複雑にしないためにこれを行ったと思いますが、実際にはお金を扱うときはdecimal



(たとえばdecimal



)を使用します。



表現の比phor 、お金を扱うことを数式を扱うこととしてシミュレートできることです。 単純な式は5 USDのように見えますが、 5 USD + 10 CHFまたは5 USD + 10 CHF + 10 USDの場合もあります。 5 CHF + 7 CHFなどの簡単な式は簡単に削減できますが、為替レートがない場合は式5 USD + 10 CHFを計算できません。 お金のトランザクションをすぐに計算しようとする代わりに、このプロジェクトでは式ツリーを作成してから、それを変換します。 おなじみですね。



Kent Beckは、彼の例では、2つのクラスでIExpression



インターフェイスIExpression



実装しIExpression



ます。





5 USD + 10 CHFを記述する場合、次のようになります。



 IExpression sum = new Sum(Money.Dollar(5), Money.Franc(10));
      
      





Money.Dollar



Money.Franc



は、 Money



オブジェクトを返す2つの静的ファクトリメソッドです。



連想性



Plus



はバイナリ演算であることに気づきましたか? 彼女をモノイドと見なしてもいいですか?



モノイドになるには、モノイドの法則を満たさなければなりません。 モノイドの最初の法則では 、操作は連想的でなければなりません。 つまり、3つのIExpression



オブジェクト、 x



y



、およびz



について、式x.Plus(y).Plus(z)



x.Plus(y.Plus(z))



と等しくなければなりません。 ここで平等をどのように理解すべきですか? Plus



メソッドの戻り値はIExpression



インターフェイスであり、インターフェイスには同等の概念はありません。 そのため、同等性は特定の実装( Money



およびSum



)に依存し、適切なメソッドを決定するか、 テスト対応 (テストパターン、 テスト固有の同等性 - 約Per。 )を使用できます。



xUnit.netテストライブラリは、カスタムコンパレーターの実装を通じてテストコンプライアンスをサポートします(ユニットテスト機能の詳細な研究のために、著者はPluralsight.comで高度なユニットテストコースを受講することをお勧めします)。 ただし、元のMoney APIには既にIExpression



型のオブジェクトを比較する機能があります!



Reduce



メソッドは、任意のIExpression



Money



型のオブジェクト(つまり、単一の通貨)に変換できます。また、 Money



はオブジェクト値であるため、 構造的に同等です (値オブジェクトとその機能の詳細については、 こちらを参照してください )。 そして、このプロパティを使用してIExpression



オブジェクトを比較できます。 必要なのは為替レートだけです。



ケント・ベックは本の中でCHFとUSDの間の2:1為替レートを使用しています。 この記事の執筆時点では、為替レートは0.96スイスフラン/ドルでしたが、サンプルコードではすべての通貨取引で整数を使用しているため、レートを1:1に丸める必要があります。 ただし、これはかなり馬鹿げた例なので、代わりに元の2:1為替レートに固執します。



次に、 Reduce



とxUnit.netの間のアダプターIEqualityComparer<IExpression>



クラスとして記述しましょう。



 public class ExpressionEqualityComparer : IEqualityComparer<IExpression> { private readonly Bank bank; public ExpressionEqualityComparer() { bank = new Bank(); bank.AddRate("CHF", "USD", 2); } public bool Equals(IExpression x, IExpression y) { var xm = bank.Reduce(x, "USD"); var ym = bank.Reduce(y, "USD"); return object.Equals(xm, ym); } public int GetHashCode(IExpression obj) { return bank.Reduce(obj, "USD").GetHashCode(); } }
      
      





コンパレータが2:1の為替レートでBank



オブジェクトを使用していることに気付きました。 Bank



クラスは、Kent Beckコードの別のオブジェクトです。 それ自体はインターフェイスを実装しませんが、 Reduce



メソッドの引数として使用されます。



テストコードを読みやすくするために、補助的な静的クラスを追加します。



 public static class Compare { public static ExpressionEqualityComparer UsingBank = new ExpressionEqualityComparer(); }
      
      





これにより、結合操作の等価性をチェックするアサートを記述できます。



 Assert.Equal( x.Plus(y).Plus(z), x.Plus(y.Plus(z)), Compare.UsingBank);
      
      





Java Aminコードのフォークで、このアサートをFsCheckテストに追加しました。これは、FsCheckが生成するすべてのSum



およびMoney



オブジェクトに使用されます。



現在の実装では、 IExpression.Plus



連想ですが、この動作が保証されていないことに注意する価値があります。理由は次のとおりですIExpression



はインターフェイスであるため、結合性に違反する3番目の実装を簡単に追加できます。 条件付きで、 Plus



操作は結合的であると想定しますが、状況は微妙です。



中立要素



IExpression.Plus



結合的であることに同意する場合、これはモノイドの候補です。 中立的な要素がある場合、それは間違いなくモノイドです。



ケントベックは例に中立的な要素を追加しなかったので、自分で追加します。



 public static class Plus { public readonly static IExpression Identity = new PlusIdentity(); private class PlusIdentity : IExpression { public IExpression Plus(IExpression addend) { return addend; } public Money Reduce(Bank bank, string to) { return new Money(0, to); } public IExpression Times(int multiplier) { return this; } } }
      
      





存在できる中立要素は1つだけなので、それをシングルトンにすることは理にかなっています。 プライベートクラスPlusIdentity



は、何もしないIExpression



新しい実装です。



Plus



メソッドは、単に入力値を返します。 これは、数字を追加するのと同じ動作です。 追加されると、ゼロは中立的な要素であり、ここでも同じことが起こります。 これは、 Reduce



メソッドでより明確に見られますReduce



メソッドでは、「中立」通貨の計算は、要求された通貨でゼロに単純に削減されます。 最後に、中立要素に何かを掛けると、中立要素が得られます。 ここで興味深いことに、 PlusIdentity



は乗算演算のニュートラル要素と同様に動作します(1)。



ここで、 IExpression



x



テストを作成します。



 Assert.Equal(x, x.Plus(Plus.Identity), Compare.UsingBank); Assert.Equal(x, Plus.Identity.Plus(x), Compare.UsingBank);
      
      





これはプロパティテストであり、FsCheckによって生成されたすべてのx



実行されます。 Plus.Identity



適用される注意はここにも適用されます: IExpression



はインターフェイスであるため、 Plus.Identity



が誰かが作成できるすべてのIExpression



実装に対して中立的な要素になるかどうかはPlus.Identity



ませんが、3つの既存の実装については、モノイドの法則が保持されます。



これで、操作IExpression.Plus



はモノイドであると言えます。



乗算



算術では、乗算演算子は「回」と呼ばれます(英語では「回」- 約Per。 )。 3 * 5と書くと、文字通り3



5回(または5



3回?)あることを意味します。 言い換えれば:

3 * 5 = 3 + 3 + 3 + 3 + 3





IExpression



同様の操作がありますか?



おそらく、モノイドとセミグループがメインライブラリの一部であるHaskell言語でヒントを見つけることができます。 後でセミグループについて学習しますが、今のところ、 Semigroup



クラスがSemigroup



stimes



関数を定義していることに注意してください。この関数はIntegral b => b -> a -> a



stimes



Integral b => b -> a -> a



型です。 つまり、整数型(16ビット整数、32ビット整数など)の場合、 stimes



関数は整数を受け取り、aの値とその値に数値を乗算します。 ここでa



は二項演算が存在する型です。



C#では、 stimes



関数はFoo



クラスのメソッドのようになります。



 public Foo Times(int multiplier)
      
      





名前stimes



文字s



stimes



意味するs



ではないSTimes



と強く疑うので、 STimes



ではなくTimes



メソッドをSemigroup



。 また、このメソッドにはIExpression.Times



メソッドと同じシグネチャがあることに注意してIExpression.Times







Haskellでそのような関数の普遍的な実装を定義できる場合、C#で同じことを行うことは可能ですか? Money



クラスでは、 Plus



メソッドを使用してTimes



を実装できます。



 public IExpression Times(int multiplier) { return Enumerable .Repeat((IExpression)this, multiplier) .Aggregate((x, y) => x.Plus(y)); }
      
      





LINQライブラリのRepeat



静的メソッドは、 multiplier



指定された回数だけthis



を返します。 戻り値はEnumerable<IExpression>



ですが、 IExpression



インターフェイスに従ってIExpression



Times



は単一のIExpression



値を返す必要があります。 Aggregate



メソッドを使用して、 Plus



メソッドを使用して2つのIExpression



値( x



およびy



)を1つに繰り返し結合します。



この実装は、以前の特定の実装ほど効果的ではありませんが、ここでは効率についてではなく、一般的な再利用された抽象化について説明します。 まったく同じ実装をSum.Times



メソッドに使用できます。



 public IExpression Times(int multiplier) { return Enumerable .Repeat((IExpression)this, multiplier) .Aggregate((x, y) => x.Plus(y)); }
      
      





これはMoney.Times



とまったく同じコードMoney.Times



。 このコードをPlusIdentity.Times



にコピーして貼り付けることもできますが、上記と同じコードなので、ここでは繰り返しません。



これは、 IExpression



からTimes



メソッドを削除できることを意味します。



 public interface IExpression { Money Reduce(Bank bank, string to); IExpression Plus(IExpression addend); }
      
      





代わりに、 拡張メソッドとして実装します:



 public static class Expression { public static IExpression Times(this IExpression exp, int multiplier) { return Enumerable .Repeat(exp, multiplier) .Aggregate((x, y) => x.Plus(y)); } }
      
      





IExpression



オブジェクトにはPlus



メソッドがあるため、これは機能します。



先ほど言ったように、これは専門的なTimes



実装よりも効果が低い可能性があります。 Haskellでは、開発者がデフォルトの実装よりも効率的なアルゴリズムを実装できるように、 タイプクラスにstimesを含めることでこれを排除しています。 C#では、 Times



をパブリック仮想(オーバーライド可能)メソッドとして使用して、 IExpression



を抽象基本クラスに再編成することにより、同じ効果を実現できます。



検証チェック



Haskell言語にはモノイドのより正式な定義があるため、単にアイデアの証拠としてHaskellでKent Beck APIを書き換えることができます。 私の最後の変更では、C#のフォークにはIExpression



3つの実装があります。





インターフェイスは拡張可能であるため、これを処理する必要があります。したがって、Haskellでは、これら3つのサブタイプをタイプsum



として実装する方が安全だと思われます。



 data Expression = Money { amount :: Int, currency :: String } | Sum { augend :: Expression, addend :: Expression } | MoneyIdentity deriving (Show)
      
      





より正式には、 Monoid



を使用してこれを行うことができますMonoid







 instance Monoid Expression where mempty = MoneyIdentity mappend MoneyIdentity y = y mappend x MoneyIdentity = x mappend xy = Sum xy
      
      





C#の例のPlus



メソッドは、 mappend



関数で表されてmappend



ます。 IExpression



クラスの残りのメンバーは、 Reduce



メソッドのみです。これは、次のように実装できます。



 import Data.Map.Strict (Map, (!)) reduce :: Ord a => Map (String, a) Int -> a -> Expression -> Int reduce bank to (Money amt cur) = amt `div` rate where rate = bank ! (cur, to) reduce bank to (Sum xy) = reduce bank to x + reduce bank to y reduce _ _ MoneyIdentity = 0
      
      





それ以外はすべてtypclassメカニズムによって処理されるため、次のようにKent Beckのテストの1つを再現できます。



 λ> let bank = fromList [(("CHF","USD"),2), (("USD", "USD"),1)] λ> let sum = stimesMonoid 2 $ MoneyPort.Sum (Money 5 "USD") (Money 10 "CHF") λ> reduce bank "USD" sum 20
      
      





stimesMonoid



すべてのSemigroup



で機能するように、 stimesMonoid



すべてのSemigroup



で定義されているため、 Expression



でも使用できます。



過去の為替レートが2:1の場合、「5ドル+ 10スイスフラン×2」は正確に20ドルになります。



まとめ



彼の本の第17章では、Kent BeckがMoney APIのさまざまなバリエーションを「表現上」にしようとする前に繰り返し考案しようとした方法を説明し、最終的に本で使用しました。 言い換えれば、彼はこの特定の問題と一般的なプログラミングの両方で多くの経験を持っていました。 明らかに、この作業は高度な資格を持つプログラマーによって行われました。



そして、彼が直感的に「モノイドデザイン」に来ているように思えるのは不思議に思えました。 おそらく彼は意図的にそれを行ったので(本でこれについては説明していません)、その優位性に気付いたという理由だけでこのデザインに来たと思います。 モノイドベースのAPIに関して非常に理解しやすいものがあるという考えを与えるため、 この例をモノイドとして具体的に検討することは私にとって興味深いようです。 概念的には、これは単に「小さな追加」です。



この記事では、9年前のコード(実際には15歳-約Lane )に戻り、モノイドとして識別しました。 次回の記事では、2015年のコードを修正します。



おわりに



これでこの記事は終わりです。 バックリンクでリンクされたHabréへの連続した投稿という形で、オリジナルと同じ方法で公開される情報がまだたくさんあります。 以下:元の記事はMark Seemann 2017であり、翻訳はJavaコミュニティによって行われ、翻訳者はEvgeny Fedorovです。



All Articles