平凡なプログラマにとっての関数型プログラミング

こないだ知人に「関数型プログラミングは一部の人が騒いでる一過性のブームだ。一般のプログラマには難しすぎて流行らない。いずれ廃れるだろう」と言われ、ぼえええ?そうかあ?それはいきなりモナドとか理解しようとしてるからでは?というか関数型=モナドとか関数型言語だと思ってない?というかこれだけ関数型っぽい設計思想が数多の言語に広がってるのにまだ廃れると思ってるの?などと色々モヤモヤしたのですが、どうせなのでそのモヤモヤを記事にしようと思いました。

この記事は私含む平均的なスキル(知識量)のプログラマが関数型プログラミングっていったい何が嬉しいの?という問いに対する簡潔な答えを考えてみたい、という主旨です。なんで平均的か?というと、高度なことは私には書けないからです。逆に言えば、関数型プログラミングやバリバリの関数型言語まで理解できなくても、関数型プログラミングを勉強するメリットはあるよ!ということになります。

ちなみに、本記事で述べている関数型プログラミングとはHaskellやF#などの関数型言語ではなく、C#、Scala、Java8のようなもうちょっとマルチパラダイムで緩い?感じのものを指しています。ここらへんは定義するのが難しいので体で感じてください。

では本題です。

表現の幅が広がるから

「表現の幅が広がる」というのが私を含む平均的で凡庸なプログラマにとって最も重要で最大のメリットだと私は思います。

関数型というとなんかすごい、と思う人もいるかもしれませんが手続き型のコードで実現出来なかったことが関数型なアプローチで出来るようになるということではありません。表現の幅が広がった事によっていろいろな設計が可能になったというのが本質だと思います。(なった、と書いていますがそもそも関数型プログラミングもオブジェクト指向その他と同じくらいの歴史を持つ方法論です)

でも表現の幅が広がると言われてもちょっと抽象的すぎますよね。もう少し具体的に書きますと、表現の幅が広がったことによって「簡潔で説明的なコードが書くことができる」というのが非常に大きなメリットと感じています。説明的なコードとは、ここでは「読んだだけで処理内容が即座に分かるようなコード」としておきます。簡単に説明的なコードを書けたら嬉しくないですか?嬉しいですよね?みなさん長ったらしいコードは嫌いですよね?プログラマの中には長ったらしいスパゲッティコードに親や兄弟を殺されたひともいらっしゃいますよね?いまこそ積年の恨みを果たすチャンスです。

なるべく易しい具体例を用いて説明しましょう。以下のC#コードを見てください。(C#erじゃなくても大体読めると思います)

var maleEmps = new List<Employee>();
for(var e in employeeList){
    if(e.gender == "M")
        maleEmps.Add(e);
}

上記は何をしたいコードでしょうか?たぶん、従業員リストの中から男性のみを抽出したいのでしょう。では以下は?

var maleEmps = new List<Employee>();
for(var e in employeeList){
    if(e.gender == "M" && e.salary > 4000000)
        maleEmps.Add(e);
}

たぶん、男性社員で年収400万以上の人を抽出したいのでしょう。

じゃあ以下は何でしょう。

var names = new List<String>();
for(var e in employeeList)
    names.Add(e.name);

これはたぶん社員の名前のリストを抽出したいんでしょう。では以下は?

var names = new List<String>();
for(var e in employeeList){
    if(e.gender == "M")
        maleEmps.Add(e.name);
}

これは男性社員の名前だけを抽出したいのでしょう。飽きてきたかもしれませんがもう一個だけ。以下は?

var sum = 0;
for(var e in employeeList){
    if(e.gender == "M")
        sum += e.salary;
}

これは男性社員の年俸の合計を計算しています。

さて、ここまでのコードを見て皆さんは大体嫌になってきたのではないでしょうか。だって、同じようなコードばかり見せられていますよね。飽きてきましたよね?飽きてください。飽きないと先に進めません。飽きてない人はもう2回くらい読み返してください。読み返しましたか?飽きましたね?飽きたら先に進みましょう。

この、同じようなというところが大きな問題だと私は思います。なぜならば、同じようなコードを繰り返し書くというのがまず面倒です。もう一つの問題は、同じようなコードなのに違うことをやっているので分かりにくいという点です。間違いさがしじゃないんですから、もうちょっとやってることが分かりやすくなってほしいですよね。

例えば一番最初のコードを例に出しますと、

var maleEmps = new List<Employee>();
for(var e in employeeList){
    if(e.gender == "M")
        maleEmps.Add(e);
}

これは男性社員でフィルタリングをしたいというコードなのですが、どこにも「フィルタリングしたい」と書いてないですよね?フィルタリングしたいならばフィルタリングしたいと書くべきじゃないですか?「読めばわかるじゃん」と言われればその通りではありますが、5行読んでようやく「フィルタリングしたい」と分かるよりだったら1単語読んで「フィルタリングしたい」と分かる方法があればそのほうが嬉しいですよね?戦時統制化で厳しい検閲の中、暗に政府批判をするような詩歌を書いた作者じゃなくて現代ビジネスで戦うんだから欧米人のようにズバズバと説明的で直接的に明確に書いてほしいですよね。

ちなみに、コードが明確になってないのでその説明不足を補うために

// 従業員を男性のみでフィルタリング
var maleEmps = new List<Employee>();
for(var e in employeeList){
    if(e.gender == "M")
        maleEmps.Add(e);
}

などとコメントを書く人がいますが、これ私は大嫌いです。説明が無いよりはあった方がいいのは当然ですが、説明を加えると言う事は管理の手間が増えると言う事です。

だいたい、後付で「従業員の名前の後ろに『さん』を付けてほしい」みたいな仕様が追加され、「動けばいいや」みたいな発想で以下のように修正する輩がやってきます。

// 従業員を男性のみでフィルタリング
var maleEmps = new List<Employee>();
for(var e in employeeList){
    if(e.gender == "M"){
        e.name += "さん";
        maleEmps.Add(e);
    }
}

するとこのコメントはうそつきになります。正しくは「フィルタリングして名前に『さん』をつける」です。だからそのようにコメントを修正しなければならないのですが、これは手間ですし、見落とすことも多いでしょう。こうやってコードの保守性というのは落ちていくものなのです。

というかそもそも、仕事で見るコードではコメントを付けること自体が偉いと勘違いしている輩もどうも多いみたいで、

if(employee.gender = "M"){
   // 男性の場合Somethingする
   doSomething();
} else if (employee.gender = "F"){
   // 女性の場合Anythingする
   doAnything();
}

みたいなコードをそれぞれのif分の中に書いているのも良く見ます。冷静に考えてみてください。このコメント意味あるんでしょうか?ラップの合いの手みたいですよ。「男性は皆Do Something (yeah, do thomething)」みたいな。なんか意味あんの?俺のリリックを聞いてほしいの?チラシの裏にでも書いてろ、な。以外の感情が生まれてきません。

だから私はコメントで処理の内容を説明するのは大嫌いで、コード自体が説明的であるべきと思います。

段々話が逸れてきたのでそろそろ関数型なコードを書きましょう。関数型のエッセンスを用いると以下のコードは

var maleEmps = new List<Employee>();
for(var e in employeeList){
    if(e.gender == "M")
        maleEmps.Add(e);
}

次のように書けます。

var maleEmps = employeeList.Where(e => e.gender == "M").ToList();

もし、ラムダ式を知らない人であれば「e => e.gender == "M"」ってなんだよ!と思うでしょうが、ちょっとそこはこらえて式の全体を見てください。よーく見てくださいよ。言語仕様を知らなくても英語知ってりゃ読めるコードですから。

「employeeListのgenderが"M"のところ(Where)だけ持ってきてリストにする(ToList)」

と読めますよね?これが私のいう「説明的なコード」です。以前は5行もあったコードが一行になり、「filterしている」と一目で何をやっているか分かるようになりました。

ただちょっと残念なのが、C#の関数はちょっと名前が変で、普通はfilterとかいう関数名になることが多いフィルタリング関数がWhereなどとSQL的な表現になっているんですね。これはちょっと分かりにくい(当社比)なのでScalaで書き直してみます。すると以下のようになります。

val maleEmps = employeeList.filter(e => e.gender == "M")

どうでしょう。これならより自然に

「employeeListをgenderが"M"という条件でフィルタリングする」

と読めるのではないでしょうか。

ちなみに、Scalaではもうちょっとこのコードを簡潔に書けまして、以下のようになります。

val maleEmps = employeeList.filter(_.gender == "M")

ここまで来るとScalaを知らない人にはアンダースコアが何をやっているか分からなくなるでしょうから、以降ではScalaを知らなくても大体読めるような冗長な形式で書いてみたいと思います。

残りの例も書き直していきましょう。

var maleEmps = new List<Employee>();
for(var e in employeeList){
    if(e.gender == "M" && e.salary > 4000000)
        maleEmps.Add(e);
}

val male400OverEmps = employeeList.filter(e => e.gender == "M" && e.salary > 4000000).toSeq

上記は条件が一つ増えただけですね。特に難しい事はありません。

var names = new List<String>();
for(var e in employeeList)
    names.Add(e.name);

val empNames = employeeList.map(e => e.name)

ここでmapが出てきました。mapという言葉はもしかしたら違和感があるかもしれませんが、あるオブジェクトを別のオブジェクトとしてマッピング(mapping)している(数学の「射影」を覚えていたらそれをイメージして頂けると良いかも)と考えれば分かりやすいかもしれません。ここでは、従業員をそれぞれの名前に変換している(従業員という世界から名前の世界にマッピングした)ということですね。

次は、フィルタリングとマッピングの組み合わせです。

var names = new List<String>();
for(var e in employeeList){
    if(e.gender == "M")
        maleEmps.Add(e.name);
}

val empNames = employeeList
                 .filter(e => e.gender == "M")
                 .map(e => e.name)

どうでしょう?以前よりも処理がずっと分かりやすくなったのではないでしょうか。

※ちなみに、Scala版のコードはwithFilterのほうが良いとかfor式を使うとか色々あるのですけれども、今回の説明には関係ないので省きました。

ここで活躍しているのはラムダ式(関数オブジェクト)高階関数です。説明すると長くなるので省きますが、雑に説明するとラムダ式とはオブジェクト化して値のように扱える匿名関数で、高階関数はラムダ式を引数に取る関数です。

この、「式をオブジェクトとして扱う」という点が非常に重要なところであります。式をオブジェクトとして扱うことが可能になったからこそ、より抽象度を高くすることが出来、filterやmapのような関数を実装することが可能になります。

ただ、最後の例はちょっと高度かもしれません。

var sum = 0;
for(var e in employeeList){
    if(e.gender == "M")
        sum += e.salary;
}

val sum = employeeList
                 .filter(e => e.gender == "M")
                 .foldLeft(0)((acc, e) => acc + e.salary)

foldLeftというのは畳み込み演算です。foldの他にreduceというのもありますが、ここでは省略します。これらは「複数の要素を一つにまとめ込む」ような汎用的な演算です。合計や平均、標準偏差や分散などの計算をイメージしてもらえれば分かりやすいと思います。上記の例では0からスタートして左の要素から次々salaryを足しこんでいくと読めば分かりやすいかと思います。

「foldLeftという語句は説明的じゃないじゃないか」とおっしゃる方もおられるでしょう。大丈夫です、合計を算出するならC#にもScalaにもsumというそのままの関数があります。

val sum = employeeList
                 .filter(e => e.gender == "M")
                 .map(e.salary)
                 .sum

さて、ここまで一般的な高階関数とコレクションを使った例を見てきました。

以上の本質とは何でしょうか?私は冒頭に説明したとおり、if, for, whileなどの制御構造を激減させたところに意義があると思っています。そもそも今回のようなケースでforを使うというのは「forを使いたい」ことが目的なのではなく、「forを使ってフィルタリング等を行いたい」からforをつかうんですよね。forは目的では無くて道具なんです。

使う道具から最後に出来上がるものを予想するのは難しいんです。お玉と包丁と鍋とピーラーから「カレーが出来る」と想像するのはかなり難しいですよね。「肉じゃがを作るんだろうな」って考える人もきっといるでしょう。forループからフィルタリングや合計を算出する処理だと見抜くのはそれに似た難しさがあります。けれども「これはカレーを作る機械です」と書いてあったら(filterと明示的に書いてあれば)誰でも「カレーが出来るんだろうな」(絞り込みをするんだろうな)と分かりますよね。

と、ここまで書くと、「えっ、じゃあfilterという名前の関数があるから嬉しいの?」と思うでしょう。そうなんです。プログラムをより抽象度の高いレベルで記述してわかりやすい名前をつけることが可能になるのが凡庸プログラマにとって一番うれしい事だと私は思います。そして、filterという6文字の名前の単純な操作をする関数を実装するためにはラムダ式と高階関数という二つの関数型プログラミングのエッセンスが必要だったんです。そして一度この方法を覚えてしまえば、高階関数を自分で定義して再利用しやすいコードを書いていくことも出来るでしょう。

つまり、関数型プログラミングを行うと手続き型のプログラミングで出来なかった何か新しいことができるようになるというわけでは無く、表現の幅が広がるということなんです。表現の幅が広がるようになったおかげで、ある処理を実装するのにより分かりやすく、より簡潔なコードが書けるようになったわけです。もちろん、関数型プログラミングにはそれ以外のメリットもありますが、ここが一番うれしいところだと私は思います。

より分かりやすく、より簡潔で説明的なコードが書けると何が嬉しいのでしょうか?たくさんのメリットがあります。

  • 保守性が上がる(久しぶりに見たコードで『ここ何やってるんだ?』と考え込むことが無くなる)
  • 生産性が上がる(単純にコード量が減るから)
  • バグが発生しにくいコードを書くことが出来る(説明的だから、自分で書いたコード量が減るから)
    - コードの再利用性(合成可能性)が向上する(関数型はより抽象度の高い方法を提供してくれる。好例がfilterやmap)
  • 並列化がしやすいコードが書ける(immutable + 参照透明な関数)

(説明がちょっと面倒になってしまったので合成可能性とかimmutableだとか説明していない語句も使ってしまいました)

上記は関数型プログラミングを実施したら勝手に付与されるフリーランチみたいな感じではないので誤解を生んでしまうかもしれません。なので納得できなければそういうことも出来る、程度に考えておくと良いかもしれません。

また、本記事では積極的に説明しないのにさらっと書いてしまいましたが、関数型プログラミングは(複数あるうちのひとつの)定義から「副作用」のない(つまり、ある関数を実行したときに内部状態が変化しない。詳しくは参照透過性でググってください)処理の組み合わせで構築されます。これにより、テストしやすいというメリットも生じます。また、副作用が無いと言う事はモジュール間の依存が少ない(無い)のでさらにコードの再利用性・合成可能性が向上します。

一方で、デメリットとしてはあまり深く考えずにimmutable(不変)なコレクションを使ったりすると計算量や記憶量を膨大に消費してしまうプログラムが出来上がってしまいます。ただ、これもList, Array, Stack, Queue, HashMapなどの各操作に関する計算量(記憶量)を覚えておく、という程度の話であってそこまで難しい事ではありません。

その他の関数型の要素

ここで説明したのはラムダ式とコレクション操作のための高階関数のほんの一部を紹介したにすぎません。これは本当に一部です。桃太郎で言えば桃が流れてきました、くらいのさわりしか説明していません。本当はOptionとかTryとかFutureくらいまで書きたかったのだけど疲れたのでやめます。書くとしたら別記事で書きます。

そのほかにも圏論だとか射とか群とか半群とかモノイドとかモナドとかfunctorとかapplicativeとか高階型とか型クラスとか数多の概念があるのですが、それらが我々に提供してくれることは「手続き処理やオブジェクト指向言語とは全く違ったプログラミングのアプローチ」です。つまり、手続き処理やオブジェクト指向に慣れ親しんだ我々にとってのメリットとしては「表現の幅を広げてくれる」ことや、「考え方の柔軟さをもたらしてくれる」ということが中心になります。決して計算機そのものの能力を拡張して何か新しいことが出来るようになるというわけではありません。

ちなみに型クラスとかモノイドについては本ブログでも多少説明しているので、興味がある人は「関数型プログラミング」のカテゴリをあさってみてください。

また、同じように抽象度の高い処理を記述して汎用的な関数を作るということに関して、以下のような秀逸な記事もありますので興味のある方はご覧ください。この例では同期・非同期、エラー処理の方法までをも抽象化できています。

モナドの本当の力を引き出す・・・モナドによる同期/非同期プログラミングの抽象化

関数型プログラミングに対する諸々への反論など

関数型プログラミングは難しい

本記事で紹介したようなコレクション操作の高階関数は難しいでしょうか?私はそうは思いません。初めて見る人は難解だと感じるかもしれませんが、初めて見て完全に理解できる人などそうそう居ないという意味での難しさでしかないと思います。ラムダ式と高階関数を使ったコレクション操作程度ならば、時間がかかったとしても1〜2時間もコードを打っていれば理解できるでしょう(少なくとも使い方だけなら)。それを難しいというのはつまり「新しい事を覚えたくない」と同じだと私は思います。もちろん、新しい事を覚えろよと主張する気もありません。それは個々人の自由です。

一方で、いきなり圏論やモナドを理解しようとしたり、高階型や型クラス等々を理解しようとするならば途端にちゃんとした専門書を買って勉強するレベルになりますから、それは難しいと表現しても良いように思います。しかしそれだけが関数型プログラミングではなく、本記事で述べたような基本的な操作も関数型プログラミングの範疇に含まれます。

「難しい事をするから偉い」ように見える(と主張しているように見える、もしくは実際に主張している)場面や人々は確かにいますが、別にそれは関数型プログラミングの世界じゃなくても、どこの世界でもいます。家電量販店などで、自分の知識と店員の知識を比べるような聞き方をして「あの店員はわかってないね」みたいに悦に浸ってるオッサンなどを見たことがあるでしょう。ああいうオッサンが皆さんに何か益をもたらしてくれる事は皆無です。居ないものと見なしましょう。

難しくて理解できない、難しくて実用に耐えないと思う部分は捨て、そこまで難しくなく実用にも耐えると判断できる部分を使えばいいだけの話です。「難しい部分があるから使えない」ではなくて、「理解できて役に立つ部分を使う」で全く問題ありません。

一方で、関数型プログラミングを勉強して理解もしたが結果全然使い物にならないな、という判断を下す人も居るかもしれません。その場合はそれで仕方ないと思います。プログラミングと一言で言っても応用分野や適用例は山のようにあり、ある分野で通ずる手法が別の分野では全く使い物にならなかったということは良くあります。プログラミングの世界では往々にしてこういうことがあるので残念でしたとしか言いようがありません。

導入してもプロジェクトメンバーが付いていけないから実用的でない

関数型プログラミングを導入する集団のスキルレベルに合わせて導入すればいいだけの話であって、手法そのものが実用的でないというのはおかしいと思います。

副作用を許さずにプログラミングするのは非効率だ

本当に、本質的にステートフルな処理のほうが適している場面であれば、その部分は関数型プログラミング以外のスタイルで書けばよいだけの話です。(というか、関数型プログラミングが必ず副作用レスというのも誤解な気がします)

先に述べたように副作用がないようなコードを記述することでテスタビリティの向上や再利用性が向上するというのは確かですが、一方で対象によってはそれを犠牲にしてステートフルな設計にした方が良い場面はたしかにあります。ハードウェアの制御などではステートマシン的な発想でコードを書くことが多々ありますし、シミュレーションゲームのAIなども必然的にステートフルな構成になりそうです。具体的には恋愛シミュレーションゲームの登場人物の感情をシミュレートする部分なんかはオブジェクト指向的な設計で超ステートフルな実装にすると思いますね。今流行りのディープラーニングだって内部状態と副作用の塊みたいなもんです。

そういう対象に当たって関数型プログラミングの考えを適用するのが難しい、もしくは一長一短があると判断した場合はそれ以外のやり易い手法(たとえばオブジェクト指向)で設計すれば良いのです。

チューリングマシンや計算機のハードの部分を多少を勉強すれば分かりますがそもそも計算機というのはステートフルなものです。あまり深く考えず、状態を持たせた方が良いところは躊躇なく状態を持たせようという柔軟な考えが肝心かと思います。ScalaやJava8、C#などのマルチパラダイム言語が「マルチパラダイム」であるというのはそういう理由なのだと私は解釈しています。

表現の幅が広がると皆好き勝手の書き方をするから良くない

表現の幅が広がるのは良い事です。

そもそもプログラマの仕事は何かというと、現実に存在する問題を計算機に解決させるために現実の問題を数学の問題に置き換えてあげることです。現実の問題を数学の世界に置き換えるので時には表現しにくいこともあるでしょう。というかそういう事がほとんどかもしれません。

なるべく表現しにくい事を、現実の世界とのギャップを少なく記述するというのがプログラマとしての腕の見せ所です。なぜならば現実とのギャップが少なく、何をしているか明確なコードは誰が見ても理解でき、保守性が向上するからです。

もし表現の幅が狭かったら適した表現を使用することが出来なかった場合、適していない表現を使って(つまり制御構造を組み合わせて)なんとか説明しなければならないので読みにくいコードとなってしまいます。ですから、プログラマにとって表現の幅が広がるというのはとても良い事だと私は思いますし、逆に制御構造を組み合わせたほうが皆好き勝手な書き方が出来てしまうと思います。

国際条約や決議などは英語とフランス語で書かれることが多いそうですが、それはフランス語には豊富な形容詞が存在しているために正確な表現で書きやすいからと聞いたことがあります。プログラミングの世界でも同じことが言えます。手続き型もオブジェクト指向も関数型もなるべく多くのパラダイムを活用できたほうが表現の幅が広がり、より場面に適していて誤解の少ない表現が書けるようになるでしょう。

それでもなお複数の表現が可能で、どの表現にも一長一短があり、かつ、統一の必要性が感じられるという場面に出くわしたらその時にプロジェクトメンバーなりコミュニティメンバーなりで統一しましょうという話をすればよいのであって、「表現の幅を増やすと良くない」という批判はおかしいと思います。

ただ、念のため付け加えておくとCPUの命令セットまで意識して時にはアセンブラで書くことも辞さない、みたいなカリカリのコードを書かなければならない、表現の幅が広がると嬉しいとか言ってたらオーバーヘッドで死ぬ、みたいな場面は別です。そういう場面ではオブジェクト指向はおろかCPUに近い手続き的なコードが最大限その良さを発揮するでしょう。

簡潔に書くのがそんなに重要なのか?関数型を覚えるために使ったコストをペイできるのか?

私は重要と思っていますし、ペイできるとも思っています。

少なくとも私は関数型をやろうと思って始めたのではなくて、長期の開発プロジェクトをなるべく楽に進めるにはどうしたら良いか?という答えを見つるのが発端でした。それで数年悩んで色々なことを経験して導き出した一つの回答が「なるべく簡潔に分かりやすい説明的なコードを書く。そのために表現の幅が広い方が好ましい。だから関数型で書けるところは書いて積極的にテストコードを回してCIしていく。可能な限りプロパティベーステストする」というものでした。だから逆に言えば、「長期の開発プロジェクトをなるべく楽に進める」他の良い方法があればそれでもいいです。

ある特定のケースについて上記の質問を問われたら、そんなことは知りません、自分で考えてくださいと答えるしかありません。

関数型プログラミングは従来のオブジェクト指向や手続き型処理から比べると概念が違いすぎて理解できる人などほとんどいない

程度の問題を置いといてかつ文面だけ見れば、正しいとも言えなくもない指摘だと思います。

しかしながら先に述べたようにプログラマの仕事というのは本来、現実世界にある問題を計算機に解かせるために数学の問題に焼きなおしてあげるという仕事であるはずです。その手法としてオブジェクト指向なり手続き処理なり関数型プログラミングなりがあるはずであって、「関数型プログラミングは従来のオブジェクト指向や手続き型処理から比べると概念が違いすぎて理解できない」というのはそういう人が居る(そして多いかもしれない)という状態の説明ではあるかもしれませんが、別にそれ自体は問題ではないと思います。

もうちょっとはっきり言えば、オブジェクト指向や手続き型処理に慣れ親しんで他の事が分からないということではないでしょうか。つまりこの指摘は「慣れ親しんだ方法を変えたくない」という感想のように思います。そう考えて昔ながらの方法でやり続けるのはその人の勝手なので私は批判できませんし、先の述べたように昔ながらの手法の方が適している場面もありますが、その一方で「慣れ親しんだ方法を変えたくないから関数型なんて覚えたくねーぞコノヤロー」とかみつかれる筋合いも無いということです。

素人には関数型プログラミングは難しい

慣れ親しんだ方法と異なる点が多いから難しいように感じる、というのが本質と思います。やったことはないですが、まったくプログラミングの事を知らない人にオブジェクト指向を教えるのと関数型プログラミングを教えるのとではあまり難易度に大差ないのではないかと個人的には思っています。関数型プログラミングを始める際はあまり先入観を持たずに学習を進めると良いかもしれません。

オブジェクト指向と関数型プログラミングは相容れない(オブジェクト指向のほうが優れている もしくは 関数型プログラミングの方が優れているという主張)

Scalaという言語がありまして、こいつはオブジェクト指向と関数型のいいとこどりをしようという発想でうまくやってます。いろんな会社でいろんなプロダクトに使われています。C#やJavaも関数型プログラミングのエッセンスを取り入れてうまいことやってます。

ただ、Java8で後になってから唐突にOptionalを導入したりしても既存資産を全部直すことは無理なので最大限活用するのは難しかったり、C#がTaskをFutureにしてモナド用の糖衣構文を用意するのではなくasync/awaitとしてTaskに依存する糖衣構文という手法で実装してしまう(→並列/並行基礎勉強会でasync/awaitをDisってきた)など、全てのパターンで完全にうまくいってるわけでもないです。Scalaは最初からオブジェクト指向と関数型を導入しようという設計なのでうまくいっているのだと思います。また、F#やHaskellがオブジェクト指向の発想を取り込むということはメリットが無いのでやらないでしょう。そういう点を見れば相容れないと言えなくもないと思います。

関数型プログラミングは学術分野や数値計算などでしか役に立たない(orそういう応用が得意)

そもそも数値計算とか学術分野とかいうくくりが漠然としすぎていて何とも言えません。

たぶん関数型という言葉の響きから特に数学っぽく聞こえるのでそう思う人もいるかもしれませんが、何度も述べたとおりコンピュータというのはそもそも計算機であって数学の問題を解くための道具です。数学を解くための道具を使って皆、音楽を聞いたりネットを見たり映画を見たりSNSをやったり絵を描いたり作曲したり人工知能を作ったりしています。それはインターネットや音楽や動画を数学の問題に置き換えてくれた先人の努力があったからです。全てのプログラミング言語は数学的です。

ただ、関数型はより数学っぽい(数学とのギャップが少ない)言語であるとは言っても良いように思うので、その点が学術分野や数値計算に適することもあるでしょうが1、同様に先に述べたような音楽やら映像やら業務支援やらの場面に適切なケースも多々あります。逆に、数値計算のお化けみたいな今話題のディープラーニングを関数型でやろうという人は居ないでしょう(ライブラリが無いだけかもしれませんが、私は本質的に関数型のメリットが活かしにくい応用例だと思っています)。

関数型プログラミングはモナドまで分かって初めて真価が発揮できる(もしくは本当の関数型プログラマはHaskell or Erlang or OCamlを使うみたいな言)

そんなことありません。理由は本記事で述べたとおりです。

オブジェクト指向 vs 関数型 みたいな議論一般について

その議論自体に何の意味があるのか分からないのでどうでもいいです。何でもそうですが、自分にとって役に立たない議論に参加することほど不毛なことはありません。役に立たないと思ったことに関わるのはやめましょう。ただ議論に打ち勝ちたいという考えだけで勉強を進めても不毛ですし、そういうことを続けていると先に述べたような家電量販店で店員に議論を吹っ掛けるおじさんとして夜な夜な徘徊する妖怪になる可能性大です。

今回のまとめ

手続き処理やオブジェクト指向に慣れ親しんだプログラマにとって関数型のフィーチャーを取り入れてみる最大のメリットは「表現の幅が広がる」ということです。

つまり、手続き処理やオブジェクト指向的な考えでは簡潔に書くことが難しく、一方で関数型的な発想では簡潔に書くのが簡単、というケースに適用すれば効果絶大です。しかしながら、関数型プログラミングは手続き処理やオブジェクト指向を亡き者にして完全なるパラダイムシフトを達成させようとするものというわけではありません。今後も手続き処理やオブジェクト指向は残り続けるでしょう。なぜならばそれが適した応用があるからです。

金子みすゞが「みんなちがって、みんないい」と言いましたが、昭和初期にすでにダイバーシティの大切さやプログラミング言語が多様であることの重要さを指摘していたのですね。というか、大体プログラミング言語に関する論争に限らず世の中のほとんどの問題はこの一言で解決できます。

私が思うに、世の中の論争のほとんどはどちらか両極端の意見を表明する人が中心になっているように思います。しかしながら本当に適切なソリューションって両極端ではなくて一見中途半端なところにあるように見えることがほとんどで、であるがために多くの人が「中途半端だなあ」という感想を持ち受け入れがたい。という構図が多いんじゃないかと。

今回の例もその一つで、タイトルは関数型大好きです!みたいな感じになっていますが私が本当に一番述べたいことは適材適所で一番良い方法を使えよ、あまり妙なこだわりを持つなよ、という事です。だから私は関数型もオブジェクト指向も手続き処理も実業務ではそれぞれ良く使います。

どの場面でどのような設計にするべきかという議論は壮大なテーマすぎるので踏み込まずにおいておきます。ただ一つだけ言うならば、従来オブジェクト指向で設計を行っていたかなりの部分で関数型に適した応用というのはありそうです。特に、動的なHTMLを吐くWebサーバーなんかは、実はオブジェクト指向設計が生きるケースはあんまりなく、かなり関数型なアプローチが活きてくるのではと思っています。


  1. 私の経験上だと、原子力発電(or 火力発電)プラントのヒートバランス計算などは数多のパラメータと計算結果を参照した計算を繰り返すので遅延評価が生きてくるパターンだと思いました。遅延評価は関数型プログラミングと関係があるのか?と言われれば少し微妙ではありますが…。