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
ます。
-
Money
は、特定の通貨での一定の金額です。 「金額」(数量)および「通貨」(通貨名)プロパティが含まれています。 これが重要なポイントです。Money
は価値オブジェクトです。 -
Sum
は、他の2つのIExpression
オブジェクトの合計です。 これには、 Augend (最初の用語)とAddend (2番目の用語)と呼ばれる2つの用語が含まれています。
式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つの実装があります。
-
Money
-
Sum
-
PlusIdentity
インターフェイスは拡張可能であるため、これを処理する必要があります。したがって、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です。