読者です 読者をやめる 読者になる 読者になる

「What Is Functional Programming? に対する反論」を読んで考えたこと

lyrical-logical.hatenablog.com

読んでいて引っかかった部分について考えました。

mutable変数は「入力とは呼べない」?

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

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

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

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

http://lyrical-logical.hatenablog.com/entry/2016/12/15/135831

問題のプログラムはこれです。

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

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

これはJavaのコード断片ですけど、断片だと分かりづらいので、JavaScriptでプログラム全体を書いてみることにします。

let Processor = {
  inbox: null,
  console: null,
  processNext() {
    let message = this.inbox
    if (message != null) {
      this.console = message
    }
  }
}

function main() {
  Processor.inbox = "hello"
  Processor.processNext()
  console.log(Processor.console)
}

main()

このコードのprocessNextメソッドは、元のコード断片より簡略化されているけど、議論の要点はおさえているはずです。

確かに、Processor.inboxはProcessor.processNextメソッドから参照可能なmutableプロパティですが、同時にmain関数からも参照可能です。このことが、mutableプロパティを一種の「共有メモリ」として使った2関数間の通信を可能にしています。

プロパティ経由の値の受け渡しが、「プログラムの状態」としてプログラミング言語のセマンティクス内で説明できる動作だとしても、Processor.inboxはProcessor.processNextへの入力となっていると表現して問題ないはずです。

このコード例から、次のことが言えます:

  • 関数の外部から書き換え可能で、関数の内部から参照可能なmutable変数は、関数への入力として使うことができる。(このような入力を副原因という。)
  • 関数の内部から書き換え可能で、関数の外部から参照可能なmutable変数は、関数からの出力として使うことができる。(このような出力を副作用という。)

副原因と副作用は対称的で、Processor.inboxはProcessor.processNextにとって副原因の源ですが、mainにとっては副作用の対象です。つまり、副原因があるとき、対になる副作用も現れ、大域的に見れば両者は同一の現象です。

「暗黙的に環境内の変数を参照するよりも、陽に変数を取るようにしたほうがいい」?

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

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

    if (message != null) {
        process(message);
    }
}
http://lyrical-logical.hatenablog.com/entry/2016/12/15/135831

環境に依存した関数の定義には注意すべし、それは一般に言って正しいことだと思うし、反論するつもりはない。しかしそれを「関数型プログラミングって何?」という文脈で話すのは、違和感が強い。

http://lyrical-logical.hatenablog.com/entry/2016/12/15/135831

ここで言っている「環境」って、何を指しているんでしょうか? グローバル環境のことでしょうか? それとも、関数の引数ではなく、ブロックスコープに宣言された変数を、環境内の変数と呼んでいるのでしょうか? あるいは、次のようなクロージャーで、関数g内で参照されているxはgの引数ではないわけですけど、こういう場合も「暗黙的に環境内の変数を参照する」に入るんでしょうか?

function f(x) {
  return function g(y) {
    return x + y
  }
}

「あらゆる関数」?

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

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

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

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

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);
    }
}

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

http://lyrical-logical.hatenablog.com/entry/2016/12/15/135831

このくだりはほとんど意味が分かりません。「関数型プログラミングって何?」の酷さを示すために、書かれたとおりにやってみて酷さを示すというにしては、そもそも書いてもいないことを突然やりだしたように見えます。「上のコードでは不足だろう」と思った理由もはっきりしなければ、それで例に出した「別の書き方」をそう書いてみたワケも、書かれていません。

“I put it to you that every function you write has two sets of inputs and two sets of outputs.” という一文中の “every” という一語が気にくわないということ以上ではないのであれば、よく分からない例なんか出さずとも、記事最初の例

public int square(int x) {
    return x * x;
}

には副原因も副作用もないよね、と指摘すれば済むのでは、と思ってしまいます。

でも、ただ「 * あらゆる * 関数に」って書き方じゃなくてevery function you writeなんだし、自分はこれは記事に食い付かせる「釣り」のたぐいの表現なんじゃないでしょうか。まあ、こういう正確さに欠けた表現はちょっと気に障るってのは分かります。

「本当にそうだろうか」?

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

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

http://lyrical-logical.hatenablog.com/entry/2016/12/15/135831

もう少し前に、こう書いてあります。

この関数には現在時刻 (new Date()) という入力が隠れている。

http://okapies.hateblo.jp/entry/2016/12/15/021550

"getSchedule" "programAt" が純粋でないなら、「この関数には現在時刻 (new Date()) という入力が隠れている」という部分で一緒に副作用(あるいは副原因)のある関数としてリストアップされなければ、話の構造上不自然です。ここでリストアップされていない以上、 "getSchedule" "programAt" は純粋だということが暗に示されてます。それを、陽に示されていないからといって「"getSchedule" "programAt" が純粋でないなら」と言い出すのは、揚げ足取りに見えます。

「一般的な説明でよいだろう」?

引数にのみ依存し値を返す、同じ引数に対して常に同じ値を返す関数であるという一般的な説明でよいだろう。

http://lyrical-logical.hatenablog.com/entry/2016/12/15/135831

「引数にのみ依存し値を返す、同じ引数に対して常に同じ値を返す関数」という定義には問題があります。この定義では、副原因を持たないが副作用を持つ、次のような関数を除外できないのでダメです。*1

let a = null

function out(x) {
  a = x
  return x
}

「読めば訳された記事がイマイチなのは分かる」?

結論。住井先生がわざわざ記事書いてくれてるんだから、まずそれを読もう。読めば訳された記事がイマイチなのは分かると思う。

http://lyrical-logical.hatenablog.com/entry/2016/12/15/135831

“A functional language actively helps you eliminate side-effects wherever possible, and tightly control them wherever it's not.”って、住井先生の「関数型言語とは、……関数型プログラミングを推奨・支援するプログラミング言語」「(関数型プログラミングとは)副作用をできるだけ(あるいは全く)用いず、式や関数で計算を表すプログラミングスタイル」ってのとほぼ同じこと言ってますよね。まあ、住井先生の記事の方はリファレンスがちゃんと張ってあるのがいいと思います。

*1:こういう説明が簡単にできるようになるので、「副作用」と「副原因」を区別するのは有意義だと思います。