マネヌモノむド


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です。

Source: https://habr.com/ru/post/J341398/


All Articles