Subscribed unsubscribe Subscribe Subscribe

ろじかるんるんものがたり

病人が特に何も書かない。無駄だからだ。

What Is Functional Programming? に対する反論

Advent Calendar の季節ですね。技術翻訳 Advent Calendar 2016 のをやっているらしく、その記事の中で、id:opapies さんが以下のような記事を書かれました(訳されました)

okapies.hateblo.jp

訳の品質は兎も角、内容が酷いなと思ったので、反論させてもらいたいと思います。元の記事を書かれた Kris Jenkins さんに届くといいんだけど。まあ別にどうでもいい。訳については、「関数型プログラミング」ではなく「関数プログラミング」と訳すべきかなあくらいしか特にいうことはない。参考。

www.slideshare.net

追記:17 ページ目のリンクを埋め込んだのに最初のページが表示される、はてなブログ君…ということで 17 ページ目を見て欲しい。


まあぶっちゃけると、そもそも英語の方の元記事を読んでいないんだけど…兎に角、さっさと始める。

追記:part2 の map, reduce の部分も含めてちゃんと読んでおきました

この記事で僕が伝えたいのは、君が書くあらゆる関数には二組の入力と二組の出力があるってことだ。

間違いなく、InboxQueue の状態はこの関数の本物の入力だ。

この隠れた入出力にはちゃんとした名前があって、その名を「副作用」という

InboxQueue は、その関数スコープから参照可能な変数の一つに過ぎない。たまたまその関数の環境から触れるというだけで、入力というよりは、環境の中の mutable な変数の一つ、以上のことはないし、これを入力とは呼べない。これはプログラムの状態だ。

言いたいことは分かる。暗黙的に環境内の変数を参照するよりも、陽に変数を取るようにしたほうがいい。例えば、以下のように。

public void processNext(InboxQueue inboxQueue) {
    Message message = inboxQueue.popMessage();

    if (message != null) {
        process(message);
    }
}

しかし * あらゆる * 関数に二つの入力がある、と記事では主張している。つまり上のコードでは不足だろう。
ならもうちょっと別の書き方をしよう。コードは疑似的なもので、こんな API がある言語は知らない。

public void processNext(Frame frame) {
    Message message = ((InboxQueue)frame.getEnvironment.getVariables.searchVariable("InboxQueue")).popMessage();

    if (message != null) {
        process(message);
    }
}||<

あるいは、InboxQueue の状態をある種の precondition としてとってもいいかもしれない。

>|java|
public void processNext(InboxQueueState state) {
    // state を使って何かしたいときもあるかもしれない。以下は一例だ。関数の意味は変わる。元記事では関数の前提条件について何も触れられていないので、これは自分が勝手に加えたものだ。popMessage は blocking API かもしれないし、そうであればこのような前提条件は妥当でない。
    if (state.isEmpty()) throw new IllegalArgumentException("InboxQueue must has some elements.");
    // これは assert を書いて precondition をコード上で陽にしているのと大した差はない。
    assert(!state.isEmpty());

    Message message = InboxQueue.popMessage();

    if (message != null) {
        process(message);
    }
}

一歩譲って、これらをもってして、あらゆる関数には二つの入力がある、という主張を受け入れたとしよう。しかしそれは「関数プログラミングにとって」大事なことなんだろうか。

そんなわけはない。グローバル変数は悪だと言われてきたし、シングルトンパターンはアンチパターンとして今では広まっている。前者はどんなパラダイムであろうと気を付けるべきだし、後者に限ってはオブジェクト指向プログラミングの事情だ。

環境に依存した関数の定義には注意すべし、それは一般に言って正しいことだと思うし、反論するつもりはない。しかしそれを「関数型プログラミングって何?」という文脈で話すのは、違和感が強い。パラダイムに限らず大切なことだからだ。その大事さは、ここでボクが長々と講釈を垂れずとも、グローバル変数やシングルトンへの悪評が保証してくれている。

というわけで、環境も入力であるというのが、妥当性のない主張とは思わないけれど、「関数型プログラミングって何?」という文脈で妥当な主張かといわれると、そうではないのではないか、というのが一つ。

次にいこう。

カプセル化とは、実装の詳細を隠蔽することだ。

実装の詳細を隠蔽すること、実装詳細の隠蔽、implementation hiding には複数の手法がある。カプセル化(encapsulation)はオブジェクト指向プログラミングにおける実装詳細の隠蔽の「手法の一つ」でしかない。
関数型プログラミングって何?」という文脈なら、関数プログラミング、もしくは関数プログラミング言語で一般的な Abstract Data Types(Algebraic Data Types と大体同じだ)による data abstraction を持ってくるのが妥当だと思う。ADT による data abstraction についてある程度知っている人なら、そも「関数プログラミングとは何か」について各自なりの答えを得てそうだけど。
別に具体的な手法に触れず、実装詳細の隠蔽という言葉を出せばよかったのではないかと思う。想定読者をオブジェクト指向プログラマだと断った上でなら、カプセル化でもよかったかもしれない。

次。

これで、この関数は入力(や出力)を隠し持たなくなった。

本当にそうだろうか。上の主張は関数内で読んでいる関数が純粋であるという仮定が成り立つときに限る。"getSchedule" "programAt" が純粋でないなら、そこには状態が潜んでいる。関数内で呼び出している関数のスコープにある状態に触れている、依存している可能性がある。

次だ。

「純粋関数」って何?

ここまでの話を踏まえたうえでなのかしらないけれど、何やら小難しいことをいっているけれど…引数にのみ依存し値を返す、同じ引数に対して常に同じ値を返す関数であるという一般的な説明でよいだろう。

さっさと次。

関数型プログラミングとは、純粋な関数を書いて隠れた入出力をなるべく取り除き、できるだけ多くのコードを入力と出力の関係だけで記述することだ。

前述の通りだ。プログラミングパラダイムに関係なく大事なことである。書籍 Effective Java に「可変性を最小限にする」という項目があったことを覚えているだろうか?

次にいこう。

関数型プログラミング言語」って何?

記事の前提が既に自分にとっては正しいとは言えないので、色々言いたいことはあるが、兎に角前提が違うのに物を語っても仕方がない。

それで全部?

そうだ。

そんなわけはない。

次、これで最後だ。

関数型言語は副作用(副原因)と戦うための道具であって、つまり:

map とか reduce があれば関数型言語になるわけではない

map や reduce を一般化していくと fold、最終的には catamorphism に行きつく。これは ADT(Algebraic Data Types) を church encoding で表現(data representation という)すれば自然に出てくるものだ。ADT は Initial F-algebra によって形式化され…これ以上は話が難しくなりすぎるのでやめよう。というか catamorphism だけでも十分難しいものが出てきてしまっている。
兎に角、これが「関数プログラミング」にとって大切かどうかと言われたら、ボクは Yes と答える。まだ「言語」については何も言及していないので、これは反論ではない。
ここからはボクの考えだけれど、関数プログラミングとは何か、と問われたときの自分なりの模範解答は「歴史的に関数プログラミング言語で行われてきた、関数プログラミング言語に一般的に備わっている機能によるプログラミング手法」となる。言語が先、パラダイムが後だ。パラダイムというのは文化的、歴史的な側面を含むと考えているので、このような主張をしている。
ADT(と pattern match)は歴史的にも現実的にも重要な言語機能であり、それを使ったプログラミングは大事なプログラミング手法だ。よって ADT と深い結びつきのある catamorphism の特殊化である map や reduce は、関数プログラミングにとって大事なものだ。
なんだか難しい話をしてしまったけれど、関数プログラミング言語で map や reduce が良く提供されてきた、利用者がよく使ってきた、これらは妥当な事実だろう。
そろそろ結論にいこう。map や reduce があれば「関数プログラミング言語」になるわけではない、という主張について反論はない。しかしそれらは「関数プログラミング」にとって大事なものだろう。「関数型プログラミングって何?」という文脈でこれらを軽視するのは妥当ではない、とだけ言わせて頂きたい。part2 の内容らしいのし全部訳されているわけではないので、でこの辺にしておく。

関数プログラミング言語とは何か、についてはちょっとしたごたごたが過去にあって、qiita (ここにあるのが悔しい…)に良い記事がある。以下の記事の内容が「ボクの考える」関数プログラミング言語とは何か、とは一致するわけではないけれど、良い記事であることには変わりないし、記事の主張も正しいものだと自分は思える。

「関数型言語」に関するFAQ形式の一般的説明 - Qiita

結論。住井先生がわざわざ記事書いてくれてるんだから、まずそれを読もう。読めば訳された記事がイマイチなのは分かると思う。記事では InboxQueue のような「状態」を「副作用」とは言っていないし、「副原因(side-cause)」なんていう独自な用語もでてこない。

蛇足:住井先生の記事について全面的に賛成するわけでもないけれど、本題から外れるし、ボクが関数プログラミングとはどのようなものだと思っているかを記述することが時間な無駄なことぐらいわかっているので、これで本当に終わる。

以上。