Pythonのモナドの詳細

良い一日!



最後のトピックでは、 Pythonを使用して多分モナドを描写しようとしました。 一般的に、タスクは達成されたように思えますが、被験者に完全に馴染みのない人が何を、そして何よりも重要な理由を理解することは困難です。 今回は、私自身の理解を統合することを含めて、モナドについてさらに詳しく説明しようとします。





この資料では、「Haskellを学ぶ」の各章の大部分を繰り返しますが、私の理解のプリズムを通して、Python言語のフレームワーク内で行います。 Haskellでこのようなタスクを作成する予定がない場合でも、本自体を読むことを強くお勧めします。視野が大幅に拡大します。 おそらく始めます。



コンテキスト



多くの場合、プログラムによって処理されるデータは何らかのコンテキストにあります。 簡単にするために、データが保存されるボックスの形で想像できます(「ボックス化された」アナロジーは完全に正確ではなく、 場合によっては適用されないこともありますが、今のところはこれに固執します)。 たとえば、リストはアイテムが入っているボックスに非常に似ています。 また、リストは特定のコンテキストを形成します。リストアイテムを操作するように設計された多くの操作は、データだけではなく、「ボックス」に取り付かれているかのようにアイテムを操作します。 フィールドに保存されているデータのボックスは、これらのフィールドが属するオブジェクトです。 関連オブジェクト上に構築されたツリーは、ブランチ/リーフに保存されたデータのコンテナです。

OOPでは、オブジェクトデータをその内部にカプセル化するのが一般的であり、オブジェクトメソッドを介してアクセスを提供することをお勧めします。 少なくとも一般的な場合、異なるクラスのオブジェクトのオブジェクトデータを操作する方法を統一することは困難ですが、データが置かれているコンテキスト(「ボックス」)として投機的に提示できるオブジェクトの場合、これは非常に実現可能です。

単純な関数をデータに適用する必要がある場合があります。これは、単純なデータ型で機能するのに十分普遍的ですが、「ボックス」内のデータでは機能しません。

例:フェルトペンと紙箱で、フェルトペンを穴から箱に刺し、そこに何かを描こうとしても意味がありません。 論理的な解決策:データを箱から取り出し、関数を適用して、元に戻します。

したがって、ボックスにコンテンツに機能を適用するメカニズムがある場合、「ボックス」はファンクターになります。



ファンクター



したがって、ファンクターは、データが配置されている特定のコンテキストの実装であり、このデータにデータを取得し、関数に適用して、コンテキストに戻すことができます。 さらに、この関数はデータ自体を操作する機能のみを必要とし、コンテキストは機能しません。



次のプロトタイプクラスを実装します。

class Functor(Infixable): ''' ''' def fmap(self, func): raise NotImplementedError() #    - 
      
      





先祖(固定可能)に注意を払う必要はありませんが、先祖オブジェクトを考慮することができます。

ファンクター内のデータに関数を適用するメカニズムは、fmapメソッドです。



ところで、Pythonのリストは最もファンクタであり、関数をリストのコンテンツに適用するメカニズムはmap()です。 map(abs、[-2、-1,0,1,2])-これはリストの要素を抽出し、それぞれに関数を適用して、リストに戻すことです。

リストをファンクターとして想像してください。

 class List(Functor): def __init__(self, *items): super(List, self).__init__() self._items = items def fmap(self, func): self._items = map(func, self._items) #     - , .. map() return self @property def result(self): return self._items
      
      





これで次のことができます。

 >>> print List(-2,-1,0,1,2).fmap(abs).fmap(lambda x: x*2).fmap(str).result ['4', '2', '0', '2', '4']
      
      







Haskellでは、型システムを使用してFunctor型クラス、およびこのクラスに属するすべてのデータ型を実装できます(また、通常は複数の型クラスに属することができます)。 使用中の型クラスメソッドは通常の関数のように見えます。

 fmap abs [-2,-1,0,1,2]
      
      





これはやや見た目は美しいですが、Pythonバージョンが適用可能です。



これで、コンテキスト内のデータに通常の関数を適用できます。 ただし、コンテキスト内のデータに関数を適用したい場合がありますが、これはコンテキスト内にもあります(データと同じ)。 つまり また、データとデータの上の機能はコンテキスト内にあります。フェルトペンと1つのボックス内の紙片の両方です。 あなたは、フェルトペンを手に入れ、紙を手に入れ、描き、結果を戻す必要があります。 私たちのボックスがこれを行うことができる場合、それは適用ファンクターです。



ここでは、1つの補助クラス、つまりInfixable(Functorの祖先にあるもの)を迂回して実装します。 そして、彼は中置記法を使用する可能性を認識する必要があります。 だから



中置記法



Pythonのカスタム関数には通常の挿入記法はありません-構文は固定されています。 そして時々私は本当に次のようなものを実装したい:

 (/*/) = lambda x,y: (x + y) * (x - y) print 5 /*/ 4 /*/ 3
      
      





ああ、まさか。 オブジェクトメソッドに中置表記法を使用できるクラスを作成しました。 クラス自体:

 class Infixable(object): INFIX_OPS = [{}] def __init__(self): self._last_is_opcode = False self._op = None table = {} for sub_table in self.INFIX_OPS: table.update(sub_table) self._op_table = table def __add__(self, val): if self._last_is_opcode: method = getattr(self, self._op_table[self._op]) method(val) else: self._op = val self._last_is_opcode = not self._last_is_opcode return self
      
      





+演算子はこのクラスでオーバーロードされ、すべてのソルトはINFIX_OPSクラスの属性に含まれています。 特定のMyObj(Infixableの子孫)がmmm(self、value)メソッドを実装し、INFIX_OPSを{'/ * /': 'mmm'、...}の形式の辞書で補足すると、インスタンスに対する一連の操作を記録するこの形式が可能になります。

 obj = MyObj() +'/*/+ 1 +'/*/'+ 2 +'/*/'+ 3
      
      





あまり美しくありませんが、機能します。 多分それから私は代わりを見つけるでしょう。



応用ファンクター



したがって、関数とデータをすぐに使用できるように実装し、関数をデータに適用して元に戻す必要があります。そして、適用可能なファンクターを取得します。 適切なクラスを実装します。 さらに、私たちの祖先には普通のファンクターがあります。紙の上に、外部のフェルトペンで絵を描くことができるのはいいことだからです。 したがって、クラス:

 class Applicative(Functor): INFIX_OPS = Functor.INFIX_OPS + [{ '<*>': 'applicate' }] def applicate(self, value): raise NotImplementedError()
      
      





このクラスの子孫はapplicate(値)メソッドを受け取り、その中置演算子は '<*>'です。

上記の先祖リストクラスをApplicativeに置き換え、新しいメソッドの実装を追加します。 これには、リストのネストレベルを下げるための補助関数が必要になります([[a、b]、[c、d]]-> [a、b、c、d])。 関数とクラス:

 def _concat(lists): return reduce(lambda x, y: x + y, lists, []) class List(Applicative): ... #      List,  Functor def applicate(self, value): # value -    ( -  ) self._items = _concat([ map(fn, value._items) for fn in self._items ]) return self
      
      





これで次のことができます。

 >>> List(str).applicate(List(1,2,3,4,5)).result ['1', '2', '3', '4', '5']
      
      





ここには、コンテキスト内のデータ(リスト内)に適用する関数がコンテキスト内にあります。

しかし、これが最も興味深いので、これを行うことができます(同時に、中置記法を適用します):

 >>> print ( ... List(str, abs) +'<*>'+ List(-10, -20) ... ).result ['-10', '-20', 10, 20]
      
      





すべての関数をすべてのパラメーターに適用した結果が得られました。 そして、あなたはこれを行うことができます:

 >>> add = lambda x: lambda y: x + y #   >>> mul = lambda x: lambda y: x * y #   >>> print ( ... List(add, mul) +'<*>'+ List(1, 2) +'<*>'+ List(10, 100) ... ).result [11, 101, 12, 102, 10, 100, 20, 200]
      
      





最初に、2つの引数のすべての関数がすべての最初の引数に適用されます。 関数はカリー化され、すべての2番目の引数に適用される2番目の引数の関数を返します!



これで、関数をコンテキストに入れ、コンテキストの値に適用できます:ボックス内の各リーフレットで、各フェルトペンをボックスから引き出し、フェルトペンを取り出します。フェルトペンは、各紙に描かれた後にのみ削除されます。



ここで状況を想像してください:図面のストリーミング制作を実装したいです。 入力シートがあると仮定すると、それらはボックスに入れられて初期コンテキストを取得します。 さらに、ライン上の各ワークプレースは、以前に箱から取り出したオブジェクトを取り出し、それを使用して新しいボックス(コンテキスト)に入れることができる機能です。 関数自体はボックスからデータを取得しません。 彼はそれらを正しく選択する方法を知りません、そしてそれはより簡単です-彼らはそれを与えてそれを処理し、新しい空の箱に入れることは多くを必要としません。

各操作は統合されたインターフェースであることがわかりました。データを取り出し->処理->結果をボックスに入れます。 前のボックスからデータを取得し、それらに関数を適用して新しいボックスを取得するだけで十分です。

通常の値を取り、結果をコンテキスト( モナド値 )で返す関数は、 モナド関数と呼ばれます 。 そして、単純な値を取り、モナド関数のチェーンを通過できる応用ファンクターはモナドです。



モナド



Monotypeクラスのプロトタイプ:

 class Monad(Applicative): INFIX_OPS = Applicative.INFIX_OPS + [{ '>>=': 'bind', '>>': 'then', }] def bind(self, monad_func): raise NotImplementedError() def then(self, monad_func): raise NotImplementedError() @property def result(self): raise NotImplementedError()
      
      





モナドは、2つのメソッドbind(>> =)、次に(>>)を提供します。

bind()はコンテキストから値を取得し、モナド関数に渡します。モナド関数はコンテキスト内の次の値(モナド値)を返します。

その後、()は前のモナド値を破棄し、引数なしで関数を呼び出し、新しいモナド値を返します。



モナドリスト



これで、 リストモナドの完全な実装が登場しました。

 def _concat(lists): return reduce(lambda x, y: x + y, lists, []) class List(Monad): def __init__(self, *items): super(List, self).__init__() self._items = items def fmap(self, func): self._items = map(func, self._items) return self def applicate(self, monad_value): self._items = _concat([ map(fn, monad_value._items) for fn in self._items ]) return self def bind(self, monad_func): self._items = _concat(map(lambda x: monad_func(x)._items, self._items)) return self def then(self, monad_func): self._items = monad_func()._items return self @property def result(self): return self._items liftList = lambda fn: lambda x: List( *(fn(x)) )
      
      





liftListは通常の関数をコンテキストに「引っ張る」:「描画」関数はモナド値を返す



リストをモナドとして使用する例を次に示します。タスクは、騎士との正確な3回の移動で、チェスボード上の1つの指定ポイントから2番目の指定ポイントに到達できるかどうかを確認することです。

 #  ,       raw_jumps = lambda (x, y): List( (x + 1, y + 2), (x + 1, y - 2), (x - 1, y + 2), (x - 1, y - 2), (x + 2, y + 1), (x + 2, y - 1), (x - 2, y + 1), (x - 2, y - 1), ) #  ,     if_valid = lambda (x, y): List( (x, y) ) if 1 <= x <= 8 and 1 <= y <= 8 else List() #          jump = lambda pos: List(pos) +'>>='+ raw_jumps +'>>='+ if_valid # ,        #     3   in3jumps = lambda pos_from, pos_to: pos_to in ( List(pos_from) +'>>='+ jump +'>>='+ jump +'>>='+ jump ).result print in3jumps((3,3), (5,1)) #  print in3jumps((3,3), (5,2)) # 
      
      







たぶんモナド



たぶん、モナドは、モナド値が2つの状態のいずれかを特徴付けるコンテキストを実装します。

-前のステップが正常に完了し、何らかの値(xのみ)

-前のステップが失敗した(何もない)



さらに、Maybeモナドのコンテキストでの一連の計算は一連のステップであり、各ステップは前のステップの結果に依存しており、すべてのステップが失敗する可能性があります。つまり、シーケンス全体が失敗します。 たぶんコンテキストでは、いずれかのステップで失敗した結果が得られた場合、後続のステップは無意味としてスキップされます。

たぶんファンクタとして、fmapを介して値に関数を適用します。値が存在する場合、値はありません(失敗した結果)-したがって、関数には適用するものが何もありません。



Maybe-context内に関数があり、Maybe内に引数がある場合(Maybe、アプリカティブファンクターのように)、関数が存在し、すべての引数がある場合は適用されます。そうでない場合、結果はすぐに失敗します。



たぶんモナドクラス:

 class Maybe(Monad): def __init__(self, just=None, nothing=False): super(Maybe, self).__init__() self._just = just self._nothing = nothing def fmap(self, func): if not self._nothing: self._just = func(self._just) return self def applicate(self, monad_value): if not self._nothing: assert isinstance(monad_value, Maybe) app_nothing, just = monad_value.result if app_nothing: self._nothing = True else: self._just = self._just(just) return self def bind(self, monad_func): if not self._nothing: monad_value = monad_func(self._just) assert isinstance(monad_value, Maybe) nothing, just = monad_value.result if nothing: self._nothing = True else: self._just = just return self def then(self, monad_func): monad_value = monad_func() assert isinstance(monad_value, Maybe) self._nothing, just = monad_value.result if not self._nothing: self._just = just return self @property def result(self): return (self._nothing, self._just) just = lambda x: Maybe(just=x) nothing = lambda: Maybe(nothing=True) liftMaybe = lambda fn: lambda x: just(fn(x))
      
      







just(x)とnothing()は、一致するモナド値をより簡単に作成するための単なるショートカットです。 liftMaybe-Maybeコンテキストに「プル」します。



ファンクターおよび適用ファンクターとしての使用例:

  def showMaybe(maybe): nothing, just = maybe.result if nothing: print "Nothing!" else: print "Just: %s" % just # ==== Maybe as functor ==== showMaybe( just(-3).fmap(abs) ) # ==== Maybe as applicative functor ==== add = lambda x: lambda y: x+y #   -   showMaybe( nothing() +'<*>'+ just(1) +'<*>'+ just(2) ) #      -   showMaybe( just(add) +'<*>'+ nothing() +'<*>'+ just(2) ) showMaybe( just(add) +'<*>'+ just(1) +'<*>'+ nothing() ) #    -    showMaybe( just(add) +'<*>'+ just(1) +'<*>'+ just(2) )
      
      







モナドとしてのMaybeの使用例を示します。 綱渡りは、棒を持ってロープに沿って歩きますが、鳥は棒の上に座って、飛び去ることができます。 棒の両側の鳥の数の差が4以下であれば、綱渡りはバランスを保つことができます。まあ、綱渡りはバナナの皮の上で滑って落ちるだけです。 「fell」/「did not fall」の形式の出力で一連のイベントをシミュレートする必要があります。 コード:

 #      to_left = lambda num: lambda (l, r): ( nothing() if abs((l + num) - r) > 4 else just((l + num, r)) ) #      to_right = lambda num: lambda (l, r): ( nothing() if abs((r + num) - l) > 4 else just((l, r + num)) ) #   banana = lambda x: nothing() #   def show(maybe): falled, pole = maybe.result print not falled #   begin = lambda: just( (0,0) ) show( begin() +'>>='+ to_left(2) +'>>='+ to_right(5) +'>>='+ to_left(-2) #    ) show( begin() +'>>='+ to_left(2) +'>>='+ to_right(5) +'>>='+ to_left(-1) ) #      show( begin() +'>>='+ to_left(2) +'>>='+ banana #    +'>>='+ to_right(5) +'>>='+ to_left(-1) )
      
      







あとがきの代わりに


同様に、他の有名なモナド、または独自のモナドを実装できます。

ファンクタを作成することはできますが、たとえば、関連するオブジェクトのツリーに対してのみです。



ご注意


私は故意にモナド法則の話題に触れませんでした、それは重要ですが、その話題はすでに膨大でした。 次回お伝えすることがあります。



変更された


コメント (ありがとう、funca)のおかげで、Listモナドのコンテキストでモナド関数によって返される値に関する矛盾を排除しました。 これで、結果としてListインスタンスを正確に返すはずです。



新バージョン


ここにあります

変更の説明:

-挿入記録の可能性を削除しました-本当に悪いようです

-Functorメソッドとmonadメソッドは、インプレースコンテキストを変更するのではなく、新しいコンテキスト値を返すようになりました。

-リストモナドはリストの継承者になりました。 したがって、モナドの結果もリストです。 T.O. リストを返す修正モナド関数を書くことができます。 モナド列の最初に必要な生成関数は1つだけで十分です。



All Articles