近況
未来が脅す右手の指 ナイフが滑る左手首
朦朧と過ぎる日曜日
消えた秒針 数えた生久伸
このままでいいのか 俺はお前は
迷路の中 IT'S MY WORLD
さ迷うがいい 無限生涯
+と-の暴害
――『KOKORO WARP』SHAKKAZOMBIE
問題
多くの人々にとって、既存の慣れ親しんだプログラミング言語で、最新のスタイルを身に着けたいと思うのが人の常だと思う。今宵、流行りのスタイルと言えば、恐らく「関数型プログラミング」になると思う。
混乱を避けるために、ここで一つ定義をする。ここで言う「関数型プログラミング」の定義とは、「副作用を出来るだけ避け、関数の連続によって書くプログラミング手法」という風にする。そして、このときの「関数」とは、「ある入力に対して、一定の出力を返すもの」という風に定義することが出来る。
さて、ここで二つの主題がある。まずひとつに、「副作用を出来るだけ避ける」という点と、「関数の連続によって書く」ということである。そして、多くの関数型プログラミングにおいて、後者は意識される。だが、前者は余りに意識されることは少ない。いや、意識されていたとしても、それを警告するための言語が余りにも少ないので、それを避けることが実質難しくなっている。
このブログをありがたくも読んでくれている読者が気になっていると思うのは、「そのプログラミング言語」とは何か、ということである。それはJavaScript、Python、Rubyといったようなものを、ここでは想定している。ちょっと釣りっぽくなってしまったが、現状として、「JavaScriptで関数型プログラミングを覚えよう」といった試みや、例えば『Software Design』の2015月8月号には、「なぜ関数型プログラミングは難しいのか」という特集が組まれている。
「副作用を避ける」ということは「イミュータブルなデータ構造を維持する」ということではないか
あまりにも抽象的な話題が多いので、実際に『Software Design』の2015月8月号に書かれた題材を見てみよう。そこでは、Pythonは次のように定義に書かれている(若干手直ししている):
def validate(data): """ >>> validate(({'email':'foobar', 'passwd': 'foo'}, []))[1] ['E-mailアドレスが不正です。', 'パスワードは8文字以上にしてください。'] """ if data[1]: return data if data[0]['email'].find('@') < 0: data[1].append('E-mailアドレスが不正です。') if len(data[0]['passwd']) < 8: data[1].append('パスワードは8文字以上にしてください。') return data def registration(data): """ >>> registration(({}, ['foobar'])) ({}, ['foobar']) >>> registration(({}, [])) ユーザー登録完了 ({}, []) """ if data[1]: return data print('ユーザー登録完了') return data if __name__ == "__main__": import doctest doctest.testmod()
公平に述べておくならば、この記事において、関数型の本質とは、すなわち「関数をお互いに接続できるようにしておく」、つまり「いくつかの関数の引数と戻り値を、同じ形にするのです。そうすることで、ある関数の戻り値を引数に、別の関数を呼び出せます。これによって、処理の流れを、関数の連続的な呼び出しとして記述できます」としている。
この部分にも、若干疑問があるのだけど(問題は、引数と戻り値を同じにすることではなく、関数の接合部分において同じ型が連続していることを保証することだとは思うのだけれど)、大筋では言わんとしたいことはわかる。問題は関数の連続性において、その手続きを記述することが、関数型プログラミングにおいて重要だという指摘だ。
しかし、同時に、そのように言うことで、「関数型的に書いた」というコードは、LispやHaskellを噛じった自分からすると、「関数型」に見えなく、むしろ「Python的」であるな、というように感じた。
どうしてかというと、このコードは、最初に述べたとおりに、リストを変更することを前提としているところだ。その部分とはappend
で、これは関数を破壊的に操作するメソッドだ。
そこで、append
を使わずにエラー処理を作ることは可能なのだろうか。
インターフェイスを型で表現する
まず最初に気になるところは、仮の入力データとエラーを格納する配列データを一緒に渡しているところった。恐らく、これは「引数と戻り値を同じにする」という記述に引っ張られてしまっている気がする。
関数をあえて、Haskell風に記述すると、次のような形で渡されればいい。
validate :: Dict -> (Dict, Array) registration :: (Dict, Array) -> (Dict, Array)
Haskell等で、型を明示することが有効なのは、このような関数の連続性を、「型」という形で抽象化できる、という側面がありそうなのがわかる。そして、これを見ればわかる通り、関数の接合点の型が合っていれば良い。
さて、問題はappend
を使わずにエラー処理を作るという問題に戻ろう。実は、それぞれのif
文を、関数で分解し、それをリスト内包記法で書けば、ちょっと効率は悪いけれども、上手く、破壊的な操作を行わずにすることが可能になる。
# valid_email :: Dict -> (String, Boolean) def valid_email(data): if data['email'].find('@') < 0: return ('E-mailアドレスが不正です。', False) else: return ('', True) # valid_password :: Dict -> (String, Boolean) def valid_password(data): if len(data['passwd']) < 8: return ('パスワードは8文字以上にしてください。', False) else: return ('', True) # validate :: Dict -> (Dict, Array) def validate(data): """ >>> validate({'email':'foobar', 'passwd': 'foo'})[1] ['E-mailアドレスが不正です。', 'パスワードは8文字以上にしてください。'] """ return data, [v(data)[0] for v in [valid_email, valid_password] if not v(data)[1]] # registration :: (Dict, Array) -> (Dict, Array) def registration(data): """ >>> registration(({}, ['foobar'])) ({}, ['foobar']) >>> registration(({}, [])) ユーザー登録完了 ({}, []) """ if data[1]: return data print('ユーザー登録完了') return data if __name__ == "__main__": import doctest doctest.testmod()
このようにすることで、不要な配列の操作と、無意味な出入力の共通化を排除することが出来た。そして、これは意図した通り、連続的なコードとして呼び出す事が出来る。
ただ、これはPython的とも言いがたく、どこかオーバーエンジニアリング的な雰囲気があるのも否めない。
多くのプログラミング言語には、破壊的操作を警告する方法はない
さて、このような破壊的な操作をついやってしまうのは、そもそも破壊的操作を使わないように遠回りするより、破壊的操作をしたほうが楽だからだ。そして、そういう風になっているものこそが、手続き型言語である、とも言えそうだ。
ちなみに、これを避けるのは難しい。実際に、自分は試しに「関数型プログラミングを小説で説明する」ということを試してみたけれど、あとから破壊的代入をしている部分を指摘されて、「あっ、しまった」と思ったりしたのだった。
偉そうにこんなことを書いている自分ですら、破壊的操作を避けて、出来るだけイミュータブルなデータ構造を保つことは難しいという実感としてあり、そして、そのように避けることそれ自体が意味あることなのか、というまた別の問題があるように感じる。
まとめ: 関数型プログラミングを「説明すること」の難しさ
今回の『Software Design』8月号を通読して痛感したのは、そもそもの「関数型プログラミング」を説明することの難しさということだった。もちろん、これが一つの「バズワード」になっていることは否めないけれども。
自分も「関数型プログラミング」の漫画のネームをシコシコと描いている状態だけれども、この手の説明の難しさ、とくに「正しくてわかりやすい」というのは難しいと思う。特にこういう過渡期の概念は、そういうのがつきまとうと思う。それこそ、同じようなことはオブジェクト指向の時だってあったんだと思う。
なので、執筆陣に対しては、そのような困難に対して格闘したことは敬意を払いつつ、やはり現状としては、OCamlやF#なんかと遊んだりしながら、「楽しく」そしてたまには「苦しみながら」覚えるのが一番の、「関数型プログラミング」を覚える道なのかなあ、と思ったりしたのだった。
参考文献
- 出版社/メーカー: 技術評論社
- 発売日: 2015/07/18
- メディア: 雑誌
- この商品を含むブログ (1件) を見る
今日買った本
- 作者: R.バード,P.ワドラー,武市正人
- 出版社/メーカー: 近代科学社
- 発売日: 1991/04
- メディア: ?行本-精装
- 購入: 7人 クリック: 82回
- この商品を含むブログ (14件) を見る
Purely Functional Data Structures
- 作者: Chris Okasaki
- 出版社/メーカー: Cambridge University Press
- 発売日: 1998/04/13
- メディア: Kindle版
- この商品を含むブログを見る
感想については追って書こうと思う。頑張って読みたい。
宣伝
ログバーでは、こういうことを語る面倒くさいエンジニアと働きたい or SwiftがなじめないというiOSエンジニアを募集しています!!