CLOSE

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

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

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

不変/永続データ構造を提供するパッケージ

寺嶋哲氏:続いて、不変/永続データ構造についてです。

listを操作する関数の問題点について、例題で見ていきましょう。add_mangoは果物名の文字列のlistをとって、mangoをそのlistに足して返します。change_from_apple_to_bananaは、同じようにlistをとってappleを探して、bananaに入れ替えて返します。

その下にfruits = [‘apple’, ‘melon’]のlistがあります。fruits1では、このlistにmangoを足します。fruits2では、appleをbananaに入れ替えたものを入れます。そうしたらfruits1とfruits2は違う中身が入っていることになるからと、assertで「!=」すると、AssertionErrorになります。

これはけっこうよくやってしまう間違いなんですが、Pythonの関数は引数が参照渡しのため、引数で渡したlistを変更してしまうと、元のlistも変更されてしまいます。

それを防ぐためには、関数呼び出しごとにlistをコピーする必要があるんですが、非効率ですね。ミスも起きがちなので、よろしくない感じがする。

この解決策が、永続データ構造というものです。永続データ構造とは、変更がある際に、変更前のバージョンを保持するデータ構造です。追加・更新・削除などの操作をすると、新しいオブジェクトが返ってきます。データが不変になることで、関数の参照透過性が上がります。

F#も基本的にはデータは不変なんですが、永続データ的な操作があんまり提供されていないので、スライドではClojureを例に出しています。Clojureでのデータ構造でVectorというものがありまして、Pythonでいうlistのようなものですね。

1行目が「def foo [0 1 2 3]」で、fooにVectorを入れています。それで「conj foo 7」とすると、Vectorに7が足されたものが新しく作られて返ってきます。それから「assoc foo 2 9」とすると、2のところが9に変更されたものが新しく返ってきます。ここで改めてfooをprintしても、変わっていない、ということになります。

さっき「参照透過性」という単語が出てきました。参照透過性とは、その式をその式の値に置き換えてもプログラムの振る舞いが変わらないことをいいます。なにがいいかというと、純粋関数という考え方があるんですね。これは、引数を受け取って、戻り値を返す以外の副作用を持たない関数のことです。例えば引数の中身を変えちゃったり、グローバル変数の状態を変えちゃったりと、ファイルの入出力などの副作用を持たない参照透過な関数のことをいいます。

純粋関数はなにがいいかというと、同じ引数を与えれば必ず同じ戻り値が返ってくるので、テストがしやすくなります。また、純粋関数を組み合わせて書かれたロジックは、解釈がしやすくなります。

Pythonには、不変/永続データ構造を提供するパッケージがあります。Pyrsistentというもので、標準モジュールのlistやtuple、dict、classなどに似せた、不変/永続/関数型のデータ型を提供するパッケージとなっています。

サンプルコードを見ていくと、まず1行目がv1 = v(1, 2, 3)でpvectorを初期化しています。それに対してv2 = v1.append(4)すると、4が足された新しいオブジェクトが返ってきます。それからv3 = v2.set(1, 5)で、index1の要素を5に変えたpvectorが返ってきます。このようにv1、v2、v3に入れたんですが、それぞれ異なるpvectorが保持されているというものになっています。

リスト操作関数を永続データ構造に置き換える

では、サンプルコードを永続データ構造に置き換えてみましょう。例題のリスト操作関数を、永続データ構造に置き換えてみました。add_mangoを見ると、PVectorをとってPVectorを返す関数になります。PVectorのappendを呼び出すと、新しく追加されたPVectorが返るので、そのままreturnできます。

change_from_apple_to_bananaも同じように、fs.setを呼ぶと新しいPVectorが返るので、そのままreturnできます。そしてfruitsを作って、fruits1にadd_mangoして、fruits2にchange_from_apple_to_bananaをしても、fruits1とfruits2が異なるものになるので、AssertionErrorではなくなります。

引数に渡したlistというかpvectorが変更されなくなるので、純粋関数となって使いやすくなったし、解釈がしやすくなりました。

Pyrsistentの実装としては、データの操作時に新しく作成したオブジェクトを返すので、パフォーマンスが問題になるんですが、なるべく見劣りしないように気をつけて実装されています。C言語拡張版も入っていて、使用可能な環境の場合は自動的に切り替わるようになっています。

PVectorの実装を例に見てみます。32個の値のlistをnodeとする木構造を共有しています。これはなぜかというと、大量データを入れてもデータ操作時、対象データに関連するノードの再作成のみであとは共有できるというメリットがあるからです。

PVector#1を見てください。PVectorはrootとtailという属性を持っていて、空の状態から値を追加していくと、まずtailに値が入ります。なぜtailのみ独立しているかというと、単純なappendの場合はパフォーマンスが稼げるためです。

値が32個を超えるとrootに入っていって、木構造を作っていきます。矢印のように「.set(1026, 新しい値)」にするとindex1026の要素が入れ替わるんですが、この赤い[ ]とlistのみ再作成で、あとは全部新しいPVectorと共有して更新後のデータを得ることができます。

dictの形によって動作を変えるには

続いて、パターンマッチについてです。予約希望の人数構成によって予約割り当てする部屋を変えたいという例題です。dictの形によって動作を変えるということです。

request1を見ると、numbersの中にadultsとchildrenが入っています。Request2はadultsのみですね。この形によって処理を切り替えたい。

Pythonの通常の実装方法だと、dictのkeyの有無をまず判断します。「Request[‘numbers’].get(’children’)」でchildrenが存在するかどうかをとって、if文でそれがNoneかどうかを調べて、childrenがいなければgeneral_roomを、childrenがいればfamilly_roomをreserveするという処理になります。この実装だと、想定されているdictのかたちがわかりづらいという問題点があります。

その解決策が、パターンマッチです。パターンマッチは、「構造を持つデータを分解して構成要素を取り出す」「構造または分解・取得したデータによって条件分岐を行う」という2つの動作を一度に行う機能です。

F#でのパターンマッチの例です。まず「type Person =」でデータ型を定義しています。Pythonでいうデータクラスのようなものですね。NameとGenderの属性を持っています。

その下がパターンマッチを使用した関数です。「match person with」でpersonに対してパターンマッチを行い、パターンに合致した矢印の右側の処理を実行します。

パターンの1行目を見ますと、Name=で、Nameはなんでもいいので、nという変数に値をキャプチャーします。Genderが1の場合はそれにmatchして、「n+” is male”という文字列が返ります。Genderが2の場合は、その下のパターンにmatchします。Genderが1でも2でもない場合は、一番下のパターンにmatchするという流れです。

つまり「genderName {Name=”Taro”; Gernder=1}」の場合は「Taro is male」という文字列が返るというサンプルです。

パターンマッチを提供するパッケージ「Pampy」

続いて、Pythonでパターンマッチを提供するパッケージについてです。Pampy……「パンピー」と読むんでしょうか、『スターウォーズ』のキャラクターから名前を取っているそうです。

Pythonでは制御構文を追加することができないので、引数でcallback関数を指定する形式になっています。文字列、数値、tuple、list、dict、dataclassなど、さまざまなデータ形式にマッチング可能です。

下が一番基本的な使い方で、match関数に対してinput、pattern、actionを指定します。inputされた値がpatternにmatchしたらactionが実行されるという流れです。patternは = [1, 2, ]となっていて、「」の部分はどんな値でもマッチするようになっています。同時に「_」の部分は値がキャプチャーされて、actionが呼び出されたときの引数に渡されることになります。なので、これを実行すると「it’s 3」という文字列が返ります。

Pampyで複数のパターンを使う場合は、patternとactionのペアを複数並べて、どんどん書いていきます。「_」単体のpatternはどんな値にもmatchするので、デフォルトのactionの定義に使うことができます。

それでは、サンプルコードをパターンマッチで書き換えてみます。想定されるデータ形式はそのままdictで書けるので、コードがとても把握しやすくなりました。想定されるデータ形式と実行するactionも近くに書けるので、そういった意味でも可読性が高くなります。

Pampyの実装を簡単に見ていきます。match関数の中でmatch_valueというものが呼ばれていて、ここが実際のmatchingを行っています。match_valueはpatternの型をまず調べ、型ごとに用意されているmatching関数を呼び出します。例えばdictの場合はmatch_dictが呼ばれますが、valueのほうがdictでない場合はその時点でmatchしないので、unmatchとなります。

そのあとはkey同士とvalue同士がmatchするかを、match_valueを再帰的に呼び出して比較していきます。patternのdictのkeyかvalueが「_」の場合は、あとでactionを呼び出すときに使うので保持しておくという処理になります。

モナドの定義

続いて、モナドです。モナドという言葉、知っている人はいますか。

(会場挙手)

おっ、こっちはいますね。よかった(笑)。泣かなくてすみました。

モナドといえばHaskellです。Haskellは、非正格な評価を特徴とする純粋関数型のプログラミング言語です。この文章は『Haskell 教養としての関数型プログラミング』の帯の引用なんですが、めっちゃかっこよくないですか。これに衝撃を受けてすぐ買ってしまいました。みなさんも本書く機会があったら、ぜひこういうかっこいい文をつけていただきたいと思います。

モナドは、Haskellで標準ライブラリとして提供されて広まりました。では、モナドとはなんでしょうか。Haskellを設計したメンバーの1人、フィリップ・ワドラー氏は言いました。「モナドは単なる自己関手の圏におけるモノイド対象だよ。なにか問題でも?」……うーん(笑)。

(会場笑)

モナドというものが数学の圏論というもので設計されているから、こういう説明になるらしいんですが、よくわかりません。なので、違う側面から説明してみます。

モナドとは、型クラスなんです。型クラスとは、データ型をカテゴライズする役割を持つ機能です。データ型が実装すべき関数のシグネチャを定義します。なのでイメージ的には、オブジェクト指向プログラミングにおけるインターフェースに相当します。が、実際にはちょっとやれることが違うので、そう言い切ってしまうと怖い人に絡まれる可能性もあるので心にしまっておいてください。

モナドの型クラスはどういう継承関係になっていくかというと、MonadはApplicative Functorを継承していて、Applicative FunctorはFunctorを継承しています。なのでFunctor、Applicative Functor、Monadの順番が、今のメジャーな説明方法です。

ではFunctorとは、どんなものか。Functorの定義があって、Functorを実際に実装する際の満たすべき規則というものがあります。

Applicative Functorの定義はこんな感じです。

Monadの定義はこんな感じで、モナドを実装するときにも満たすべき規則がありますが、説明していると時間が足りないので省略します。今回は、モナドの使い方に着目して説明を行います。

Maybeモナドのバインド関数の実装

改めて、モナドとはなにか。モナドとは、文脈を持った値について、文脈を持たない値について想定した関数を適用したり、関数を組み合わせたりする関数(演算子)を定義した型クラスです。

実際の例を見ていきます。「Maybeモナド」というものがあります。Maybeは、値が存在する/存在しない、という文脈を持った型です。下がMaybeのデータ型の定義です。

「data Maybe a = Just a | Nothing」の「Maybe a」は「Just a」もしくは「Nothing」の値のどちらかになります。Pythonでいうユニオン型みたいな感じですね。値が存在しない時はNothingで、存在するときは「Just 実際の値」という書き方になります。下は、x+yが100を超える場合にNothingを返す関数を定義してみた例です。

1行目が型シグネチャで、addLimitはint引数を2つとって、Maybe int型の戻り値を返すという関数です。その下が「addLimit x y」でパターンマッチの書き方になっていて、x+yが100以下の場合はJust(x + y)を、それ以外の場合はNothingを返す関数になっています。

この関数を、バインド関数というもので結合することができます。バインド関数とは、左辺の文脈付きの値を、右辺の文脈なし引数を取り文脈付き値を返す関数に適用する演算子です。

実際のサンプルでは、まずバインド関数の左辺がJust 10で、Maybeという文脈付きの値になっています。addLimitは、int型をとって、文脈付きのMaybe intを返す関数でした。なので、バインド関数で結合することができます。

すると、Just1 10、addLimit 20で、Just 30が返ります。それから、さらに結合してJust 60が返ります。2行目の例でいくと、Just 20とaddLimit 90で、100を超えているのでNothingが返りますね。NothingとaddLimit 60を結合するんですが、Maybeモナドのバインド関数の実装でおもしろいところはここなんです。

左辺値がNothingの場合はもう関数をバイパスして、そのままNothingを返すという実装になっています。なので、結合したい関数が例えばすごくコストが高いものでも、一旦Nothingになったらもう呼び出されない、というふうにすることができます。

Pythonのモナド

ここで、やっとPythonのモナドにたどり着きました(笑)。typesafe-monadというパッケージがあります。これはPyMonadというものをベースに型アノテーションをつけたもので、代表的ないくつかのモナドを実装しています。

例題として、存在しない可能性のある値を取得する関数を組み合わせてみましょう。get_userはint型のuser_idをとって、Noneかuserのobject自体を返します。get_user_photoはuserをとって、Noneかuser_photoを返します。get_photo_datetimeも同じように、user_photoをとってdatetimeを返します。

これらを単純に組み合わせると、if文でNoneか調べて、値が存在したら次の関数を実行するというように、if文ごとにネストが深くなってしまいます。

そこで、Maybeモナドを使って書き換えてみます。まずは関数を、文脈なしの引数をとって文脈ありの戻り値を返す関数に書き換えていきます。get_userだと、user_id: intをとって、文脈ありの型、Maybe[User]を返します。これはオブジェクトが存在する場合はJustで返して、存在しない場合はNothingを返すという書き換えです。

同じようにget_user_photoとget_photo_datetimeを書き換えます。するとbind関数で結合して、if文のネストがなくなりました。

Pythonでは新しい記号を使った演算子が作成できないので、bind関数を「>>」で繋いでいます。関数合成のときもこの記号を使いましたね。新しい演算子が増やせないぶん、パッケージ間で使用する記号がかぶりがちになります。

実装を見ていきますと、Functor、Applicative、Monadで必要なメソッドを、空のメソッドとして定義します。そして、具体的なモナドのクラスですね。Maybeクラスでbind関数などの必要なメソッドの中身をオーバーライドして実装しています。文脈付きの値、Just、Nothingなどはそのクラスを継承して実装しているかたちです。

今回、時間の都合で説明をいろいろ省略したので、どこかで「Pythonで一から学ぶモナド」を発表したいと思っています。

まとめに入ります。関数型プログラミングは、いろいろな便利な機能があります。Pythonでのプログラミングでも、パッケージを使って簡単に利用することができます。そしてなにより関数型プログラミングは、楽しいです。

今回の解説は、機能やパッケージの使い方の入り口にすぎないので、興味が出てきた方はぜひ一緒に、関数型プログラミング沼に飛び込んでみましょう。「Meguro.LYAHFF(すごいHaskell本の原書を読む会}」でもいいですし、SQUEEZEブースでもお待ちしております。ご静聴ありがとうございました。

(会場拍手)

関数型プログラムはPythonicではない?

司会者:ありがとうございました。質問がある方は、挙手にてお願いします。

(会場挙手)

質問者1:ありがとうございました。僕もPythonも好きだし、関数型プログラムも好きなので、すごくおもしろい発表でした。抽象的な質問になってしまうんですが、Pythonを実際書いていると、Pythonは精神的に関数型プログラムに向いてない感じがすることがあるんです。

例えばPythonicな書き方と言われるものは、内包表記だったりして、マップとかフィルターでガンガン公開関数を重ねていく書き方ではなかったり。複数行に渡って1つの式を書くときも、( )でくくったりしなきゃいけなくて面倒くさいし。あと、お話にもありましたが、無名関数を書くときにlambdaという単語をわざわざ書かなきゃいけなかったり。

今回の発表は、そのへんをハックしてやりやすくするという部分もあったと思うんですが、あんまり「ファンクショナルプログラミングってPythonicな書き方じゃないのかな」と思うことがあるんです。抽象的で申し訳ないですが、どう思われますか?

寺嶋:いや、まったくそのとおりだと思います。なのでPythonには、こういう便利な記法とか、便利な関数型機能が取り入れられていないと思うんです。関数型言語が好きな人の病気として、自分がふだん使っている言語で「あの機能を使えないかな」と探しちゃうので、それを紹介したいというところが今日のテーマでした。

関数型言語機能は、別にふだんの仕事で使えなくても「こういう考え方があって、こういう書き方ができます」と覚えておくと、ふだんのプログラミングでも「こういう書き方ができたな」と、知らないときよりきれいに書けることがあるので、いいとは思っています。

司会者:ほかに質問がある方は挙手にてお願いします。

(会場挙手)

質問者2:発表ありがとうございます。先ほどの質問とも近いかもしれませんが、実際に紹介されたライブラリで、プロダクションコードで使っているものはありますか?

寺嶋:プロダクションコードで、私が実際に書くときに使ってるものはないです。ただ、プロダクションコードに含まれている依存パッケージの中で、Pyrsistentを使っているJSON Schemaというパッケージがあります。こちらはどこかのキャッシュの仕組みで永続データ構造を使っているみたいです。

質問者2:ありがとうございます。

司会者:時間となりましたので、これにて終了とさせていただきます。ありがとうございました。

(会場拍手)

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

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

無料会員登録

会員の方はこちら

この記事のスピーカー

同じログの記事

コミュニティ情報

Brand Topics

Brand Topics

  • 今までとこれからで、エンジニアに求められる「スキル」の違い AI時代のエンジニアの未来と生存戦略のカギとは

人気の記事

新着イベント

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

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

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