15分でモナド

エントリー


YOW!で 2013年 Haskell言語の開発者の一人、 教授。 Philip Wadlerは、モナドによって、純粋な関数型言語が入出力や例外処理などの本質的に命令型の操作を実行できるようにする方法を示しました。 当然のことながら、このトピックに対する聴衆の関心は、インターネット上のモナドに関する出版物の爆発的な成長をもたらしました。 残念ながら、これらの出版物のほとんどは関数型言語で書かれた例を使用しており、関数型プログラミングの初心者はモナドについて学びたいと考えています。 しかし、モナドはHaskellや関数型言語に固有のものではなく、命令型プログラミング言語の例で説明できるかもしれません。 これがこのガイドの目的です。

このガイドは他とどのように違いますか? 直観とPythonコードの基本的な例をいくつか使用して、15分以内にモナドを「開く」ようにします。 したがって、 ブリトー宇宙服 、内臓機能について議論し、哲学を理論化して掘り下げることはしません。

動機付けの例


機能の構成に関連する3つの問題を検討します。 それらを2つの方法で解決します。通常の命令型とモナドの使用です。 次に、異なるアプローチを比較します。

1.ロギング


3つの単項関数があると仮定します: f1f2 、およびf3 、それぞれ数字を取り、それをそれぞれ1、2、および3 f3増やして返します。 各関数は、完了した操作に関するレポートであるメッセージも生成します。
 def f1(x): return (x + 1, str(x) + "+1") def f2(x): return (x + 2, str(x) + "+2") def f3(x): return (x + 3, str(x) + "+3") 

それらを連鎖させてパラメータxを処理したい、言い換えると、 x+1+2+3を計算したいと思います。 さらに、各機能が何をしたのかを人間が読める形式で説明する必要があります。

次の方法で必要な結果を達成できます。
 log = "Ops:" res, log1 = f1(x) log += log1 + ";" res, log2 = f2(res) log += log2 + ";" res, log3 = f3(res) log += log3 + ";" print(res, log) 

このソリューションは、多数の単調なミドルウェアで構成されているため、理想的ではありません。 チェーンに新しい関数を追加する場合、このリンクコードを繰り返す必要があります。 さらに、 res変数とlog変数をlogした操作はコードの可読性を損ない、プログラムのメインロジックに従うことが難しくなります。

理想的には、プログラムはf3(f2(f1(x)))ような単純な一連の関数のように見えるはずです。 残念ながら、 f1およびf2によって返されるデータ型は、パラメーター型f2およびf3と一致しません。 しかし、新しい関数をチェーンに追加できます。
 def unit(x): return (x, "Ops:") def bind(t, f): res = f(t[0]) return (res[0], t[1] + res[1] + ";") 

これで、次のように問題を解決できます。
 print(bind(bind(bind(unit(x), f1), f2), f3)) 

次の図は、 x=0発生する計算プロセスを示していx=0 。 ここで、 v1v2 、およびv3は、 unitおよびbind呼び出しの結果として取得された値です。



unit関数は、入力パラメーターxを数値と文字列のタプルに変換します。 bind関数は、渡された関数をパラメーターとして呼び出し、結果を中間変数t蓄積します。

bind機能にミドルウェアを配置することで、ミドルウェアの繰り返しを避けることができました。 ここで、関数f4を取得した場合、それをチェーンに含めるだけです。
 bind(f4, bind(f3, ... )) 

そして、他の変更を加える必要はありません。

2.中間値のリスト


また、単純な単項関数を使用してこの例を開始します。
 def f1(x): return x + 1 def f2(x): return x + 2 def f3(x): return x + 3 

前の例のように、 x+1+2+3を計算するためにこれらの関数を構成する必要があります。 また、関数、つまりxx+1x+1+2およびx+1+2+3の作業の結果として取得されたすべての値のリストを取得する必要があります。

前の例とは異なり、関数は構成可能です。つまり、入力パラメーターのタイプは結果のタイプと一致します。 したがって、単純なチェーンf3(f2(f1(x)))が最終結果を返します。 ただし、この場合、中間値は失われます。

「額」の問題を解決しましょう。
 lst = [x] res = f1(x) lst.append(res) res = f2(res) lst.append(res) res = f3(res) lst.append(res) print(res, lst) 

残念ながら、このソリューションには多くのミドルウェアも含まれています。 また、 f4を追加することにした場合、中間値の正しいリストを取得するには、このコードを繰り返す必要があります。

したがって、前の例のように、2つの追加機能を追加します。
 def unit(x): return (x, [x]) def bind(t, f): res = f(t[0]) return (res, t[1] + [res]) 

次に、プログラムを一連の呼び出しとして書き直します。
 print(bind(bind(bind(unit(x), f1), f2), f3)) 

次の図は、 x=0発生する計算プロセスを示していx=0 。 繰り返しますが、 v1v2およびv3は、 unit呼び出しおよびbind呼び出しから取得した値を示します。



3.空の値


今回はクラスとオブジェクトを使用して、より興味深い例を示してみましょう。 2つのメソッドを持つEmployeeクラスがあるとします:
 class Employee: def get_boss(self): # Return the employee's boss def get_wage(self): # Compute the wage 

Employeeクラスの各オブジェクトには、マネージャー( Employeeクラスの別のオブジェクト)と給与があり、適切なメソッドを通じてアクセスされます。 どちらのメソッドもNone返すことができます(従業員にはマネージャーがいないため、給与は不明です)。

この例では、この従業員のリーダーの給与を表示するプログラムを作成します。 マネージャーが見つからない場合、またはマネージャーの給与を決定できない場合、プログラムはNoneを返します。

理想的には、次のようなものを書く必要があります
 print(john.get_boss().get_wage()) 

ただし、この場合、いずれかのメソッドがNone返すと、プログラムはエラーで終了します。

この状況を処理する明白な方法は次のようになります。
 result = None if john is not None and john.get_boss() is not None and john.get_boss().get_wage() is not None: result = john.get_boss().get_wage() print(result) 

この場合、 get_bossおよびget_wageへの追加の呼び出しを許可しget_wage 。 これらの方法が十分に重い場合(たとえば、データベースへのアクセス)、当社のソリューションは行いません。 したがって、変更します。
 result = None if john is not None: boss = john.get_boss() if boss is not None: wage = boss.get_wage() if wage is not None: result = wage print(result) 

このコードは計算の点では最適ですが、3つのネストされたifために読み取りが不十分です。 したがって、前の例と同じトリックを使用しようとします。 2つの関数を定義します。
 def unit(e): return e def bind(e, f): return None if e is None else f(e) 

そして、ソリューション全体を1行にまとめることができます。
 print(bind(bind(unit(john), Employee.get_boss), Employee.get_wage)) 

既にお気づきかもしれませんが、この場合、 unit関数を記述する必要はありません。単に入力パラメーターを返すだけです。 しかし、経験を一般化するのがより簡単になるように、それを残します。

Pythonでは、メソッドを関数として使用できるため、 john.get_boss()代わりにEmployee.get_boss(john)を記述できることにも注意してください。

次の図は、Johnにリーダーがない場合、つまりjohn.get_boss()None返す場合の計算プロセスを示しています。



結論


同じタイプの関数f1f2fnを組み合わせたいとします。 入力パラメーターが結果と同じ場合、 fn(… f2(f1(x)) …)という形式の単純なチェーンを使用できます。 次の図は、 v1v2vnとして示される中間結果を含む一般化された計算プロセスを示しています。



多くの場合、このアプローチは適用されません。 最初の例のように、入力値のタイプと関数の結果は異なる場合があります。 または、関数を構成することもできますが、例2と例3で中間値の集合と空の値のチェックをそれぞれ挿入したように、呼び出し間に追加のロジックを挿入します。

1.命令的な決定


すべての例で、最初に最も単純なアプローチを使用しました。これは次の図で表すことができます。



f1を呼び出す前にf1いくつかの初期化を行いました。 最初の例では、変数を初期化して一般的なログを保存し、2番目の例では中間値のリストを作成しました。 その後、特定の接続コードで関数呼び出しを散在させました:集計値を計算し、 Noneの結果を確認しました。

2.モナド


例で見たように、命令的な決定は常に冗長性、繰り返し、および混乱するロジックに悩まされていました。 より洗練されたコードを取得するために、特定のデザインパターンを使用し、それに応じてunitbind 2つの関数を作成しました。 このテンプレートはモナドと呼ばれます。 unitが初期化を実装している間、 bind機能にはミドルウェアが含まれています。 これにより、最終的なソリューションを1行に簡素化できます。
 bind(bind( ... bind(bind(unit(x), f1), f2) ... fn-1), fn) 

計算プロセスは、ダイアグラムで表すことができます。



unit(x)呼び出すと、 v1初期値が生成されます。 次に、 bind(v1, f1)は新しい中間値v2生成します。これは、 bind(v2, f2)次の呼び出しで使用されます。 このプロセスは、最終結果が得られるまで続きます。 さまざまなunitを定義し、このテンプレートのフレームワーク内でbindことにより、さまざまな機能を1つの計算チェーンに結合できます。 Monadライブラリ( PyMonadやOSlash-およそTransl。など )には、通常、特定の関数構成を実装bindための、すぐに使用できるモナド( unitbind関数のペア)が含まれています。

関数をチェーンするには、 unitおよびbindによって返される値は、 bindの入力パラメーターと同じ型である必要があります。 このタイプはモナドと呼ばれます。 上記の図に関して、変数v1v2vn型はモナド型でなければなりません。

最後に、モナドを使用して記述されたコードを改善する方法を検討してください。 明らかに、繰り返されるbind呼び出しは見栄えがよくありません。 これを回避するには、別の外部関数を定義します。
 def pipeline(e, *functions): for f in functions: e = bind(e, f) return e 

代わりに
 bind(bind(bind(bind(unit(x), f1), f2), f3), f4) 

次の略語を使用できます。
 pipeline(unit(x), f1, f2, f3, f4) 


おわりに


モナドは、関数を構成するために使用されるシンプルで強力なデザインパターンです。 宣言型プログラミング言語では、ロギングや入出力などの命令型メカニズムの実装に役立ちます。 命令型言語で
同じタイプの関数の一連の呼び出しをリンクするコードを一般化および短縮するのに役立ちます。

この記事は、モナドの表面的で直感的な理解のみを提供します。 詳細については、次のソースに連絡してください。

  1. ウィキペディア
  2. Pythonのモナド(素晴らしい構文を使用!)
  3. Monadチュートリアルのタイムライン

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


All Articles