CLOSE

Pythonで始めてみよう関数型プログラミング(全2記事)

Pythonを使って関数型プログラミング Part.1

2019年9月16、17日、日本最大のPythonの祭典である「PyCon JP 2019」が開催されました。「Python New Era」をキャッチコピーに、日本だけでなく世界各地からPythonエンジニアたちが一堂に会し、さまざまな知見を共有します。プレゼンテーション「Pythonで始めてみよう関数型プログラミング」に登壇したのは、株式会社SQUEEZEの寺嶋哲氏。講演資料はこちら

なぜ関数型プログラミングを愛するようになったのか

寺嶋哲氏:それでは発表を始めます。まず「おまえ誰よ?」。はい、寺嶋哲といいます。Twitterなどでは「@meganehouser」というIDでやっています。

所属は株式会社SQUEEZE(スクイーズ)で、ふだんはPython、Django、Django REST frameworkでバックエンドを書いて、AngularJS、Angularでフロントエンドをちょっと書いています。

Meguro.LYAHFGGというイベントを主催しています。『すごいHaskell本』という書籍があるんですが、その原書がオンラインで無料公開されているので、それを何人かで読んでみようという会です。

そんな私ですが、なぜ関数型プログラミングを愛するようになったのか。話は10年以上前にさかのぼります。私の前職はSIerで、アプリケーション開発をしていました。動作環境は.NET Frameworkなので、当然のように言語はC#を使っていました。

そこで素敵なプログラミング言語に出会ってしまったんですね。(スライドを指して)このロゴの言語がなにかわかる人、いますか?

……ゼロですね。泣きそう(笑)。

これは、F#という言語です。F#はOCamlの影響を色濃く受けた.NET Framework向けの言語で、手続型・オブジェクト指向・関数型に対応した、強い静的型付けのマルチパラダイムプログラミング言語です。REPLがあったり、コンパイルして実行ファイルにできたり、スクリプト実行もそのままできるので、いろいろ便利に使えます。

私はこの言語で関数型プログラミングの魅力に目覚めました。パイプライン演算子が、最近いくつかの言語で実装されて評判になっていますが、F#が広めた機能ですね。

Pythonと同じでオフサイドルールなので親しみが持てたり、関数型言語機能でコードがコンパクトに書けて「関数型プログラミングめっちゃいいじゃん」となりました。

この発表の目的としては、まず、便利な関数型言語の機能を知っていただきます。次に、Pythonを使って関数型言語スタイルでプログラミングする方法の概要を知っていただきます。

それから、Pythonの関数型言語を実現するパッケージの使い方、実現方法の概要を知っていただきます。最後に一番大きなゴールとして、みなさんが関数型プログラミングを試してみたくなることを目指しています。

目次です。まず関数型プログラミングについて概説します。その次に、Pythonで関数型プログラミングを実現する方法をいくつか説明します。そのあとに関数型プログラミングの機能を1つずつ取り上げて紹介して、まとめになります。

注意として、この発表では関数型プログラミングの機能紹介のために、Python以外のソースがいっぱい出てきます。雰囲気で理解してください。

関数型プログラミングとは

それではさっそく本題です。関数型プログラミングとは、プログラミングパラダイムの1つです。例えば手続き型プログラミングは、命令実行の列として上から下にプログラムを記述していくスタイルです。それに対して関数型プログラミングは、複数の式を関数の適用によって組み合わせていくプログラミングスタイルとなります。

なので、特定の目的のためのデータ型をユーザーが定義したり、関数を組み合わせて新しく関数を作るといった機能が豊富という特徴があります。

関数型プログラミングの実際のサンプルを見ていただきます。このサンプルコードでは、F#のパイプライン演算子を使っています。

1行目から見ていきますと、「let add x y」、addというxとyの引数をとる関数を定義しています。右側の「= x + y」で「x+y」が関数の実体になっているので、addは引数を2つとって、x+yの足し算をして返すという関数です。2行目も同じように「minus」を定義して、3行目「display」で引数をprintする関数になっています。

その次が、普通に関数を組み合わせた例です。実際はadd、minus、displayの順に実行されるのに、( )で入れ子になっているので、逆になっていて読みにくいですね。

パイプライン演算子を利用した書き方だと、( )の入れ子がなくなって、このパイプライン演算子という記号が矢印に見えて、処理の順序に書けるので読みやすくなります。このように関数を組み合わせてプログラミングしていくのが、関数型プログラミングです。

Pythonでの関数型プログラミングの4つの実現方法

それでは、Pythonでの関数型プログラミングの実現方法に入っていきます。4つの方法があるので、1つずつ説明していきます。

まず1つ目が、標準機能と標準パッケージで実現する方法です。こちらは公式ドキュメントに解説が掲載されています。

続いて2つ目が、Pythonを生成するコンパイラで実現する方法です。これはPythonの実行までの生成物を作る、新たな言語になります。

CPythonでは、実行するまでの生成物としてはまずPythonコード(.py)があります。これがパースされてAbstract Syntax Tree、ASTになり、最後にそれがコンパイルされてBytecode(.pyc)ファイルになります。

おもしろいのは、この生成物を各コンパイル対象にしている言語が存在しているということですね。

まず、Pythonコードにコンパイルするプログラミング言語として、Coconutという言語があります。Pythonと互換性があって、Pythonコードがそのまま実行できます。その上で関数型言語の便利な機能を追加して書けるので、ふだん使いにも便利な言語だと、私は思っています。

続いて、PythonのASTにコンパイルするプログラミング言語としては、Hylangがあります。文法は完全にLispで、Lispを強力な言語としているマクロも入っています。次の技術書典で「日本Hyユーザ会」というところがHylangの書籍を出すらしいので、楽しみにしています。

CPythonのBytecodeにコンパイルするプログラミング言語としては、dgがあります。Haskellのような見た目の動的言語です。「Slow,Stupid」と公式ドキュメントのトップに書いてあるので、仕事で本気で使う言語ではないような雰囲気です。

3つ目の実現方法は、AST変換で実現する方法です。MacroPy3という、Pythonでマクロを実現するモジュールがあります。モジュールのインポートフックでASTを変換して、マクロを実現しています。

図を見ていきますと、まずソースがあって、パースしてASTになります。インポートされたときにそのフックを行って、マクロがASTを変換して、New ASTにしてコンパイルに持っていくということになります。

インポートされたときに変換が走るので、マクロを使用したコードはメインモジュールでは使用できない制限があります。マクロのサンプルとして、関数型機能のマクロが提供されています。

4つ目は、がんばってサードパーティパッケージとして実現する方法です。これは通常のパッケージと同じようにpipインストールして、インポートすればそのまま使えるようになるのでとっても手軽ですね。この発表で主に取り上げるのは、この実現方法です。

Pythonで関数合成

それでは、関数型プログラミング機能の紹介に入っていきます。まず1つ目は、関数合成です。

関数型プログラミングは、関数を組み合わせるスタイルでした。

例題として、複数の割引関数を適用したあとの価格が高額か少額か判定してみましょう。1行目がappleのtupleです。1つ目の要素が果物の種類で、2つ目の要素が価格です。

その下にdiscount関数が2つ並んでいます。discount_by_dayがfruitをとって、曜日によって割引になる関数です。ここでは土日の場合に割引になります。discount_by_timeが時間帯によって割引になる関数です。ここでは22時以降の場合に割引になります。

最後に、fruitの価格が300円以上であればHighを、以下であればLowを出すという関数があります。

これらの関数を単純に組み合わせただけでは、( )の入れ子になって可読性が低くなります。実行の順序が関数の並び順と逆になっているところが問題点です。

これを解決する機能が関数合成です。関数合成は、専用の演算子を使用して2つの関数を合体させて、新しい1つの関数を作り出す機能です。関数を処理する順序で並べて定義できるので、可読性が高くなります。

以下はF#での関数合成のサンプルコードです。まず「add50」「add100」「minus100」で関数を定義しています。その下の「let newFunc = add50 >> add100 >> minus100」が関数合成をしている行です。この「>>」が関数合成の演算子で、add50とadd100を合成させてできた関数を、さらにminus100に合成しています。なのでnewFuncは、全体としてはintの引数を1個とって、intの戻り値を返す関数になります。

F#などの関数型言語の場合は、記号を組み合わせて新たな演算子を定義することができます。関数合成の演算子も、言語仕様内で定義することができます。

実装方法としては、関数の定義と同じようにまず演算子名を置いて、その後ろに引数を置きます。1個目の引数が左辺で、2個目のgが右辺の関数ですね。xが、呼び出されるときの引数です。右辺の定義は、まず左辺のfの関数に引数を適用して、その戻り値がgの引数として渡されるという組み合わせ方です。

それではPythonで関数合成をしていきましょう。fn.pyというパッケージがあります。Googleで「fn.py」と検索すると、一番最初に「pip3 install fn」が出てしまうんですが、こちらはアクティブじゃないフォークのほうが入ってしまうので、うまく動かなかったりします。アクティブなのは「pip3 install fn.py」です。

fn.pyはGitHubに「Missing features of fp in Python」と書かれているとおり、いくつもの関数型機能を実装した便利パッケージになっています。

それではfn.pyで関数合成をします。fn.pyのFクラスというもので関数合成ができます。下は、例題を関数合成で書き換えたコードです。「new_func=」が関数合成にしたところです。ほぼF#と同じように「>>」で関数を合成しています。違いは、最初にFクラスのオブジェクトをつないでいるところですね。

Fクラスの関数合成の実装方法について説明します。Pythonでは、演算子は特殊メソッドのオーバーロードによって実現します。新たな記号を使用した演算子を増やすことはできません。Fクラスの「_rshift_」をオーバーロードして「>>」の演算子の機能を上書きしています。

下が実装コードです。composeというメソッドで関数を組み合わせています。ここの組み合わせ方はF#のコードと同じような感じですね。これを「_rshift_」の中で呼び出して「>>」で関数を合成できるようにしています。

関数のカリー化

続いて、関数のカリー化についてです。discount関数に、関数の引数が増えたらどうなるでしょうか。例として、rateで割引率も引数で指定できるようにしたい場合を考えます。

関数合成は、左辺の関数の結果を右辺の関数の引数としているので、引数が1個の場合を想定しているんです。なので、Pythonではlambda式で引数を1個の関数にする必要があります。ただ、簡単な定型的な動作なのにけっこう追加する文字が多いし、何回もlambdaと打つのは面倒という問題点があります。

その解決策が、カリー化された関数です。F#を例にすると、F#の関数は標準でカリー化された関数になっており、引数の部分適用ができます。カリー化とはなにかはいったん置いておいて、「部分適用できる」ということが重要な点になります。

部分適用は、複数の引数を必要とする関数に対して、一部の引数のみ適用することができるという機能です。部分適用した関数は、残りの引数を必要とする関数になります。

F#のサンプルで説明します。1行目の「add x y」で2引数の関数を定義しています。2行目で、addに1個目の引数だけ渡しています。すると、add 5はyだけをとる関数になって、最終的に3+5で8が返るようになります。

では、カリー化とはなにか。Wikipediaから引用してきました。「カリー化とは、複数の引数をとる関数を、引数が『もとの関数の最初の引数』で戻り値が『もとの関数の残りの引数を取り結果を返す関数であるような関数』にすることである」。

……ひと息で言えました(笑)。

(会場笑)

わかりにくいので、サンプルで説明しますね。F#の例ですが、まず「let add x y z」で、3引数の関数を定義しています。普通だと「3つの引数をとって1つの戻り値を返す」という考え方ですが、カリー化された関数は、引数が1つの関数なんですね。

引数を1個とって関数を返します、というものがカリー化された関数で、1個とっては残りの引数をとる関数を返すという繰り返しなので、例えば1個だけ渡すとか、途中の2個だけ渡すということができます。

では、Pythonで関数をカリー化してみましょう。fn.pyにcurriedデコレータというものがあって、これで関数をカリー化できます。サンプルコードで見ますと、sum5という5引数の関数に対してcurriedデコレータを渡すと、1個ずつ引数を適用したり、何個かずつ引数を適用したりできるようになります。

例題を解決していきます。例題の割引関数にcurriedデコレータをつけて、カリー化します。すると関数合成が、部分適用によって1つの引数をとる関数になるので、スッキリと関数合成できるようになります。

fn.pyのcurriedの実装方法としては、厳密にはカリー化は行なっていません。標準モジュールのfunctools.partialを使って引数を部分適用しています。引数がすべて揃った場合には、引数を元の関数に与えて戻り値をreturnします。揃っていない場合は、引数を部分適用した関数をさらにcurreidでラップしてreturnするという実装です。

「functools.partialってなんだっけ?」という人もいると思いますが、これは関数に引数の部分適用したCallableオブジェクトを返すものです。サンプルでは、int関数に「base=2」、その引数を部分適用してbasetwoに入れています。そしてbasetwoに2進数の文字列を渡すと、引数2でパースされて10進数の数字が返ります。

おまけです。Fクラスの説明で「関数合成と『部分適用』を簡単に使えるようになる関数ラッパークラス」と言いました。実は、関数合成では部分適用を使うことが多いので、Fクラスには部分適用の機能が付いています。構成する関数を1つ目の要素にして、2つ目以降を部分適用したい引数のtupleにすると、部分適用されて関数合成することができます。

ちょっと喋りすぎたので、ここで休憩に入ります。最近Fitbitを買ったんですけど、今、心拍数117です(笑)。

(会場笑)

だいぶ緊張してます(笑)。

続きを読むには会員登録
(無料)が必要です。

会員登録していただくと、すべての記事が制限なく閲覧でき、
著者フォローや記事の保存機能など、便利な機能がご利用いただけます。

無料会員登録

会員の方はこちら

この記事のスピーカー

同じログの記事

コミュニティ情報

Brand Topics

Brand Topics

  • 大変な現場作業も「動画を撮るだけ」で一瞬で完了 労働者不足のインフラ管理を変える、急成長スタートアップの挑戦 

人気の記事

新着イベント

ログミーBusinessに
記事掲載しませんか?

イベント・インタビュー・対談 etc.

“編集しない編集”で、
スピーカーの「意図をそのまま」お届け!