関数型プログラミングのためのPythonライブラリf

こんにちは同僚!







簡潔な名前f



持つPythonライブラリについて説明します 。 これは、機能的なスタイルで問題を解決するための関数とクラスを備えた小さなパッケージです。







-何、Python用の別の機能ライブラリですか? 著者は、 fn.pyがあり、一般にこれらの機能的なクラフトが100万あることを知っていますか?







-はい、知っています。







ライブラリの外観の理由



私はかなり長い間Pythonを練習してきましたが、数年前、関数型プログラミング、特にKlyuchに真剣に夢中になりました。 FPで採用されたいくつかのアプローチは私に非常に強い印象を与えたので、私はそれらを日常の開発に移したいと思いました。







ある言語のパターンが別の言語のパターンに大まかに埋め込まれている場合、その原則とコーディング合意を考慮せずにアプローチを受け入れないことを強調します。 FPがどれほど好きであっても、これを機能的なスタイルとして流そうとするために、大量のマップとラムダに悩まされています。







そのため、同僚の抵抗に合わないように機能を整えようとしました。 たとえば、FPに精通していない人の理解を促進するために、マップとリダクションの代わりに条件付きの標準サイクルを使用します。







その結果、図書館の一部の部分は戦闘プロジェクトにあり、おそらくはまだ使用中でした。 最初に、それらをプロジェクトからプロジェクトにコピーし、次に関数とスニペットのファイルダンプを開始し、最後にライブラリ、Pypiのパッケージ、およびドキュメントですべてを設計しました。







一般的な情報



ライブラリは純粋なPythonで書かれており、以下を含むすべてのOSで動作します ヴィンドゥスに。 Pythonの両方のブランチがサポートされています。 具体的には、バージョン2.6、2.7、および3.5をチェックしました。 他のバージョンで問題が発生した場合はお知らせください。 唯一の依存関係は、両方のブランチのすぐ下にあるアジャイル開発用のsix



パッケージです。







ライブラリは、pipを介して標準的な方法でインストールされます。







 pip install f
      
      





すべての関数とクラスは、headモジュールで利用できます。 それは覚えておく必要がないことを意味します

エンティティへのパス:







 import f f.pcall(...) f.maybe(...) f.io_wraps(...) fL[1, 2, 3]
      
      





パッケージには、次のサブシステムが搭載されています。









以下のセクションでは、コメント付きのコード例を示します。







機能



別のエコシステムからPythonに転送した最初の関数は、Lua言語からのpcall



。 私は数年前にプログラミングしましたが、言語は機能していませんが、喜んでいます。







pcall(保護された呼び出し)関数は、別の関数を取り、ペア(err, result)



を返します。errはエラーで、 result



空、またはその逆です。 JavascriptやGowなどの他の言語でのこのアプローチには精通しています。







 import f f.pcall(lambda a, b: a / b, 4, 2) >>> (None, 2) f.pcall(lambda a, b: a / b, 4, 0) >>> (ZeroDivisionError('integer division or modulo by zero'), None)
      
      





この関数を、例外をスローする既に記述された関数のデコレーターとして使用すると便利です。







 @f.pcall_wraps def func(a, b): return a / b func(4, 2) >>> (None, 2) func(4, 0) >>> (ZeroDivisionError('integer division or modulo by zero'), None)
      
      





破壊的な構文を使用すると、署名レベルで結果を解凍できます。







 def process((err, result)): if err: logger.exception(err) return 0 return result + 42 process(func(4, 2))
      
      





残念ながら、破壊的な構文は3番目のPythonで削除されています。 手動で解凍する必要があります。







興味深いことに、ペア(err, result)



は、 Either



モナドに他なりません。







以下は、より現実的なpcall



例です。 多くの場合、HTTP要求を作成し、ジェイソンからデータ構造を取得する必要があります。 リクエスト中に多くのエラーが発生する可能性があります。









4つの例外をキャッチしてtryで呼び出しをラップするということは、コードを完全に読み取り不可能にすることを意味します。 遅かれ早かれ、何かを傍受するのを忘れ、プログラムがクラッシュします。 以下は、ほぼ実際のコードの例です。 ローカルレストサービスからユーザーを取得します。 結果は常にペアになります。







 @f.pcall_wraps def get_user(use_id): resp = requests.get("http://local.auth.server", params={"id": user_id}, timeout=3) if not resp.ok: raise IOError("<log HTTP code and body here>") data = resp.json() if "error" in data: raise BusinesException("<log here data>") return data
      
      





他のライブラリ関数を検討してください。 f.achain



f.ichain



を強調しf.achain



f.ichain



ます。 どちらも、チェーン内のオブジェクトから安全にデータを取得するように設計されています。







次のモデルのdjangoがあるとします:







 Order => Office => Department => Chief
      
      





同時に、すべてのフィールドがnot null



not null



隣接するフィールドを安全に確認できます。







 order = Order.objects.get(id=42) boss_name = order.office.department.chief.name
      
      





はい、私はselect_related



知っていますが、それは問題ではありません。 この状況は、ORMだけでなく、他のクラス構造にも当てはまります。







ある顧客がいくつかのリンクを空にすることを要求するまで、それは私たちのプロジェクトにありました。これらは彼のビジネスの特徴だからです。 データベース内のフィールドをnullable



、簡単に降りることができて嬉しかったです。 もちろん、急ぎのために、空のリンクを持つモデルの単体テストを作成しませんでした。古いテストでは、モデルは正しく入力されていました。 クライアントは更新されたモデルで作業を開始し、エラーを受け取りました。







f.achain



関数は、属性チェーンを安全に走査します。







 f.achain(model, 'office', 'department', 'chief', 'name') >>> John
      
      





チェーンが壊れている場合(フィールドがNone、存在しない)、結果はNoneになります。







f.ichain



アナログf.ichain



は、一連のインデックスを実行します。 彼女は辞書、リスト、およびタプルを使用します。 この関数は、jasonから取得したデータを操作するのに便利です。







 data = json.loads('''{"result": [{"kids": [{"age": 7, "name": "Leo"}, {"age": 1, "name": "Ann"}], "name": "Ivan"}, {"kids": null, "name": "Juan"}]}''') f.ichain(data, 'result', 0, 'kids', 0, 'age') >>> 7 f.ichain(data, 'result', 0, 'kids', 42, 'dunno') >> None
      
      





私は両方の機能をクラッチからget-in



。クラッチの祖先はget-in



と呼ばget-in



ます。 便利な点は、マイクロサーバーアーキテクチャでは、応答の構造が絶えず変化しており、常識に対応していない場合があることです。







たとえば、応答には、ネストされたフィールドを持つユーザーオブジェクトフィールドがあります。 ただし、何らかの理由でユーザーがいない場合、フィールドは空のオブジェクトではなく、なしになります。 次のようなUい構造:







 data.get('user', {]}).get('address', {}).get('street', '<unknown>')
      
      





私たちのバージョンは読みやすいです:







 f.ichain(data, 'user', 'address', 'street') or '<unknown>'
      
      





Kluzhaからライブラリf



渡される2つのスレッドマクロ: ->



および->>



。 ライブラリでは、それらはf.arr1



およびf.arr2



と呼ばれます。 両方とも、初期値を関数形式に渡します 。 Lispのこの用語は、後で計算される式を意味します。







言い換えれば、 フォームfunc



関数またはフォームのタプル(func, arg1, arg2, ...)



いずれか(func, arg1, arg2, ...)



。 このフォームは、凍結式のような場所に渡して、後で変更して計算できます。 Lispのマクロのようなものが判明しますが、非常に悲惨です。







f.arr1



は値(およびさらに結果)を最初の値f.arr1



置き換えます

フォーム引数:







 f.arr1( -42, #   (lambda a, b: a + b, 2), #  abs, #  str, #  ) >>> "40"
      
      





f.arr2



は同じことを行いますが、値をフォームの最後にf.arr2



します。







 f.arr2( -2, abs, (lambda a, b: a + b, 2), str, ("000".replace, "0") ) >>> "444"
      
      





次に、 f.comp



関数は関数の構成を返します。







 comp = f.comp(abs, (lambda x: x * 2), str) comp(-42) >>> "84"
      
      





f.every_pred



はスーパー述語を構築します。 これは、すべての内部述語が真である場合にのみ真となる述語です。







 pred1 = f.p_gt(0) #   pred2 = f.p_even #  pred3 = f.p_not_eq(666) #   666 every = f.every_pred(pred1, pred2, pred3) result = filter(every, (-1, 1, -2, 2, 3, 4, 666, -3, 1, 2)) tuple(result) >>> (2, 4, 2)
      
      





スーパー述語は怠laです。最初の偽の値で計算の連鎖を中断します。 上記の例では、 predicate.py



モジュールのpredicate.py



が使用されています。これについては後で説明します。







f.transduce



関数は、Claudeのトランスデューサー(トランスフォーマー)パターンを実装しようとする素朴な試みです。 要するに、 transducer



map



機能とreduce



機能の組み合わせです。 それらの重ね合わせにより、「中間データのないものからあらゆるものへ」という原則に従って変換が行われます。







 f.transduce( (lambda x: x + 1), (lambda res, item: res + str(item)), (1, 2, 3), "" ) >>> "234"
      
      





関数のモジュールf.nth f.nth



とその同義語:コレクション要素への安全なアクセスのためのf.first



f.second



およびf.third









 f.first((1, 2, 3)) >>> 1 f.second((1, 2, 3)) >>> 2 f.third((1, 2, 3)) >>> 3 f.nth(0, [1, 2, 3]) >>> 1 f.nth(9, [1, 2, 3]) >>> None
      
      





述語







は、trueまたはfalseを返す式です。 述語は、数学、論理、および関数型プログラミングで使用されます。 多くの場合、述語は高階関数の変数として渡されます。







ライブラリに最も必要な述語のいくつかを追加しました。 述部の動作が最初の引数に依存する場合、述部は単項 (パラメーターなし)および二項 (またはパラメトリック)になります。







単項述語を使用した例を検討してください。







 f.p_str("test") >>> True f.p_str(0) >>> False f.p_str(u"test") >>> True #  ,    int  float  f.p_num(1), f.p_num(1.0) >>> True, True f.p_list([]) >>> True f.p_truth(1) >>> True f.p_truth(None) >>> False f.p_none(None) >>> True
      
      





バイナリになりました。 何かがゼロより大きいと主張する新しい述語を作成します。 いったい何? まだ知られていない、これは抽象化です。







 p = f.p_gt(0)
      
      





次に、述語を使用して、任意の値を確認します。







 p(1), p(100), p(0), p(-1) >>> True, True, False, False
      
      





類推によって:







 # -    : p = f.p_gte(0) p(0), p(1), p(-1) >>> True, True, False #    : p = f.p_eq(42) p(42), p(False) >>> True, False #    : ob1 = object() p = f.p_is(ob1) p(object()) >>> False p(ob1) >>> True #      : p = f.p_in((1, 2, 3)) p(1), p(3) >>> True, True p(4) >>> False
      
      





すべての述語の例は示しませんが、それは退屈で長くなります。 述語は、合成関数f.comp



、スーパー述語f.every_pred



、組み込みfilter



関数、および以下で説明する汎用f.every_pred



と完全にf.comp



ます。







ジェネリック



ジェネリック(general、generalized)は、結果を計算するためのいくつかの戦略を持つ、呼び出されるオブジェクトです。 戦略の選択は、入力パラメーター(構成、タイプ、または値)に基づいて決定されます。 ジェネリックは、渡されたパラメーターに対して他に見つからない場合、デフォルト戦略の存在を想定します。







Pythonでは、すぐに使用できるジェネリックはなく、特に必要ありません。 Pythonは、入力値の関数を選択する独自のシステムを構築するのに十分な柔軟性を備えています。 それでも、Common Lispでのジェネリックの実装が非常に好きだったので、スポーツの関心からライブラリで同様のことをすることにしました。







こんな感じです。 まず、ジェネリックのインスタンスを作成します。







 gen = f.Generic()
      
      





次に、特定のハンドラーで拡張します。 .extend



デコレータは、このハンドラーの一連の述語を引数ごとに1つ受け取ります。







 @gen.extend(f.p_int, f.p_str) def handler1(x, y): return str(x) + y @gen.extend(f.p_int, f.p_int) def handler2(x, y): return x + y @gen.extend(f.p_str, f.p_str) def handler3(x, y): return x + y + x + y @gen.extend(f.p_str) def handler4(x): return "-".join(reversed(x)) @gen.extend() def handler5(): return 42
      
      





内部のロジックは単純です。デコレータは、割り当てられた述語とともに関数を内部辞書にファイルします。 これで、ジェネリックは任意の引数で呼び出すことができます。 呼び出されると、関数は同じ数の予測子で検索されます。 各述語が対応する引数に対してtrueを返す場合、戦略が見つかったと見なされます。 見つかった関数を呼び出した結果が返されます:







 gen(1, "2") >>> "12" gen(1, 2) >>> 3 gen("fiz", "baz") >>> "fizbazfizbaz" gen("hello") >>> "olleh" gen() >>> 42
      
      





戦略が出ない場合はどうなりますか? デフォルトのハンドラーが設定されているかどうかによって異なります。 このようなハンドラーは、任意の数の引数を満たす準備ができている必要があります。







 gen(1, 2, 3, 4) >>> TypeError exception goes here... @gen.default def default_handler(*args): return "default" gen(1, 2, 3, 4) >>> "default"
      
      





装飾後、関数はジェネリックのインスタンスになります。 興味深いトリックは、ある戦略の実行を別の戦略に移すことができるということです。 Claude、Erlang、Haskellのように、いくつかのボディで機能することがわかります。







None



を渡すと、以下のハンドラーが呼び出されます。 ただし、内部では2つのintを持つ別のハンドラーにリダイレクトされます。これはhandler2



です。 これにより、引数の合計が返されます。







 @gen.extend(f.p_none) def handler6(x): return gen(1, 2) gen(None) >>> 3
      
      





コレクション



このライブラリは、リスト、タプル、辞書、およびセットに基づいた「強化された」コレクションを提供します。 改善とは、各コレクションの動作における追加のメソッドといくつかの機能を意味します。







改善されたコレクションは、クラスを呼び出すことによって通常のコレクションから作成されるか、角括弧で囲まれた特別な構文によって作成されます。







 fL[1, 2, 3] #  f.List([1, 2, 3]) >>> List[1, 2, 3] fT[1, 2, 3] #  f.Tuple([1, 2, 3]) >>> Tuple(1, 2, 3) fS[1, 2, 3] #  f.Set((1, 2, 3)) >>> Set{1, 2, 3} fD[1: 2, 2: 3] >>> Dict{1: 2, 2: 3} #  f.Dict({1: 2, 2: 3})
      
      





コレクションには、 .join



.foreach



.map



.filter



.reduce



.sum



ます。







リストとタプルは、さらに.reversed



.sorted



.group



.distinct



および.apply



ます。







メソッドを使用すると、関数に渡さずにコレクションから結果を取得できます。







 l1 = fL[1, 2, 3] l1.map(str).join("-") >>> "1-2-3"
      
      





 result = [] def collect(x, delta=0): result.append(x + delta) l1.foreach(collect, delta=1) result == [2, 3, 4] >>> True
      
      





 l1.group(2) >>> List[List[1, 2], List[3]]
      
      





私は各メソッドのリストを退屈させません。希望する人はコメント付きでソースコードを見ることができます。







メソッドが同じコレクションの新しいインスタンスを返すことが重要です。 これにより、彼女が誤って変わる可能性が低くなります。 操作.map



またはリストの他の操作は、リスト、タプル、タプルなどを返します。







 fL[1, 2, 3].filter(f.p_even) >>> List[2]
      
      





 fS[1, 2, 3].filter(f.p_even) >>> Set{2}
      
      





辞書は、私がいつも夢見ていたペア(, )



反復処理し(, )









 fD[1: 1, 2: 2, 0: 2].filter(lambda (k, v): k + v == 2) >>> Dict{0: 2, 1: 1}
      
      





改善されたコレクションは、他のコレクションと積み重ねることができます。 結果は、この(左)タイプの新しいコレクションになります。







 #   fD(a=1, b=2, c=3) + {"d": 4, "e": 5, "f": 5} >>> Dict{'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4, 'f': 5} #  +   fS[1, 2, 3] + ["a", 1, "b", 3, "c"] >>> Set{'a', 1, 2, 3, 'c', 'b'} #     fL[1, 2, 3] + (4, ) List[1, 2, 3, 4]
      
      





どのコレクションも別のコレクションに切り替えることができます。







 fL["a", 1, "b", 2].group(2).D() >>> Dict{"a": 1, "b": 2} fL[1, 2, 3, 3, 2, 1].S().T() >>> Tuple[1, 2, 3]
      
      





コンボ!







 fL("abc").map(ord).map(str).reversed().join("-") >>> "99-98-97"
      
      





 def pred(pair): k, v = pair return k == "1" and v == "2" fL[4, 3, 2, 1].map(str).reversed() \ .group(2).Dict().filter(pred) >>> Dict{"1": "2"}
      
      





モナド



ライブラリの最後で最も難しいセクション。 モナドに関する一連の記事を読んだ後、私もそれらをライブラリに追加しようと思いました。 同時に、彼は次の逸脱を許可しました。









たぶん



たぶん、モナドはオプションとしても知られています。 このクラスのモナドは、2つのインスタンスで表されます。Just (またはSome)は、興味のある肯定的な結果のリポジトリです。 何も(他の言語では-なし) -空の結果。







簡単な例。 モナドコンストラクターを定義する- スカラー(フラット)値をモナドに変換するオブジェクト:







 MaybeInt = f.maybe(f.p_int)
      
      





別の方法では、 unit 、またはモナドユニットと呼ばれます。 モナド値を取得します:







 MaybeInt(2) >>> Just[2] MaybeInt("not an int") >>> Nothing
      
      





良い結果になるのは、intのテストに合格することだけです。 それでは、 モナドパイプラインを実際に試してみましょう。







 MaybeInt(2) >> (lambda x: MaybeInt(x + 2)) >>> Just[4] MaybeInt(2) >> (lambda x: f.Nothing()) >> (lambda x: MaybeInt(x + 2)) >>> Nothing
      
      





例からわかるように、 Nothing



チェーンの実行を中断しNothing



。 完全に正確に言うと、チェーンは途切れず、最後まで進み、各ステップでNothing



返されNothing









モナドデコレータで任意の関数をカバーして、そこからスカラーのモナド表現を取得できます。 以下の例では、デコレーターは、戻り値のintのみが成功と見なされるようにします。この値はJust



、他のすべてはNothing



ます。







 @f.maybe_wraps(f.p_num) def mdiv(a, b): if b: return a / b else: return None mdiv(4, 2) >>> Just[2] mdiv(4, 0) >>> Nothing
      
      





>>



演算子は、 モナドバインディングとも呼ばれ、 .bind



メソッドによって呼び出されます。







 MaybeInt(2).bind(lambda x: MaybeInt(x + 1)) >>> Just[3]
      
      





>>



.bind



メソッドは、関数だけでなく、 関数の形をとることもできます。







 MaybeInt(6) >> (mdiv, 2) >>> Just[3] MaybeInt(6).bind(mdiv, 2) >>> Just[3]
      
      





モナドからスカラー値を解放するには、 .get



メソッドを使用します。 モナドの古典的な定義には含まれておらず、一種のinであることに注意することが重要です。 .get



メソッドは、厳密にパイプラインの最後にある必要があります。







 m = MaybeInt(2) >> (lambda x: MaybeInt(x + 2)) m.get() >>> 3
      
      





どちらか



このモナドは前のものを拡張します。 たぶん問題は否定的な結果が破棄されることですが、私たちは常に理由を知りたいです。 どちらも、左と右、左と右の値のサブタイプで構成されます。 左の値は負のケースを担当し、右の値は正のケースを担当します。







このルールは、「私たちの原因は正しい(つまり、真)」というフレーズで覚えやすいです。 英語の正しい言葉は「忠実」も意味します。







そして、ここに過去からのフラッシュバックがあります:同意して、それは記事の冒頭からカップル(err, result)



を思い出させますか? Javascriptでのコールバック? Gowの呼び出し結果(異なる順序でのみ)?







それは同じです。 これらはすべてモナドですが、コンテナや数学的な装置なしでは装飾されていません。







Either



モナドは、主にエラーをキャッチするために使用されます。 誤った値は左に進み、組立ラインの結果になります。 正しい結果は、次の計算のために右側にスクランブルされます。







Bothモナドコンストラクターは、左の値と右の2つの述語を受け入れます。 以下の例では、文字列値は左の値に、数値は右に移動します。







 EitherStrNum = f.either(f.p_str, f.p_num) EitherStrNum("error") >>> Left[error] EitherStrNum(42) >>> Right[42]
      
      





コンベアを確認します。







 EitherStrNum(1) >> (lambda x: EitherStrNum(x + 1)) >>> Right[2] EitherStrNum(1) >> (lambda x: EitherStrNum("error")) \ >> (lambda x: EitherStrNum(x + 1)) >>> Left[error]
      
      





f.either_wraps



デコレータは、関数をモナドコンストラクタにします。







 @f.either_wraps(f.p_str, f.p_num) def ediv(a, b): if b == 0: return "Div by zero: %s / %s" % (a, b) else: return a / b @f.either_wraps(f.p_str, f.p_num) def esqrt(a): if a < 0: return "Negative number: %s" % a else: return math.sqrt(a) EitherStrNum(16) >> (ediv, 4) >> esqrt >>> Right[2.0] EitherStrNum(16) >> (ediv, 0) >> esqrt >>> Left[Div by zero: 16 / 0]
      
      





IO



IOモナド(入出力)は、ファイルの読み取り、キーボード入力、画面への印刷など、データの入出力を分離します。 たとえば、ユーザー名を要求する必要があります。 モナドがなければ、 raw_input



ですが、これは抽象化を減らし、副作用でコードを詰まらせます。







キーボード入力を分離する方法は次のとおりです。







 IoPrompt = f.io(lambda prompt: raw_input(prompt)) IoPrompt("Your name: ") #  .   "Ivan"   RET >>> IO[Ivan]
      
      





モナドを取得したので、それをさらに組立ラインに転送できます。 次の例では、名前を入力して表示します。 f.io_wraps



デコレータは、関数をモナドコンストラクターに変換します。







 import sys @f.io_wraps def input(msg): return raw_input(msg) @f.io_wraps def write(text, chan): chan.write(text) input("name: ") >> (write, sys.stdout) >>> name: Ivan #   >>> Ivan #   >>> IO[None] # 
      
      





エラー



エラーモナド、Try(エラー、試行)も実用的な観点から非常に便利です。 例外を分離し、計算の結果が内部に正しい値を持つSuccess



インスタンス、またはFailture



れた例外を持つFailture



であることを確認します。







Maybe andeitherと同様に、モナドパイプラインは肯定的な結果のためにのみ実行されます。







, . Success



, Failture



:







 Error = f.error(lambda a, b: a / b) Error(4, 2) >>> Success[2] Error(4, 0) >>> Failture[integer division or modulo by zero]
      
      





.get



Failture



. ? .recover



:







 Error(4, 0).get() ZeroDivisionError: integer division or modulo by zero # value variant Error(4, 0).recover(ZeroDivisionError, 42) Success[2]
      
      





( ), . Success



. . , Success



. :







 def handler(e): logger.exception(e) return 0 Error(4, 0).recover((ZeroDivisionError, TypeError), handler) >>> Success[0]
      
      





. :







 @f.error_wraps def tdiv(a, b): return a / b @f.error_wraps def tsqrt(a): return math.sqrt(a) tdiv(16, 4) >> tsqrt >>> Success[2.0] tsqrt(16).bind(tdiv, 2) >>> Success[2.0]
      
      







, . , , ? ?







do-, . :







 def mfunc1(a): return f.Just(a) def mfunc2(a): return f.Just(a + 1) def mfunc3(a, b): return f.Just(a + b) mfunc1(1) >> (lambda x: mfunc2(x) >> (lambda y: mfunc3(x, y))) # 1 2 1 2 >>> Just[3]
      
      





, mfunc3



, . x



y



. .







おわりに



, f



. , . , . — .







. — . Pypi .







, .







. ご清聴ありがとうございました。








All Articles