「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:こういう説明が簡単にできるようになるので、「副作用」と「副原因」を区別するのは有意義だと思います。

游ゴシックは何故Windowsでかすれて見えるのか

TL;DR

游ゴシック体は単に細いから薄いのではなく、ガンマ補正が2重、3重に掛かっているために、グレーが本来よりも明るくなりすぎている。ガンマ補正を逆に掛けると、正常な表示になる。

かすれた游ゴシック

Windowsでは游ゴシックがかすれて見える。細字だと薄くて読みづらいから、より太いウェイトを指定しろという話もある。(Windowsで游ゴシックが汚いのは、結局誰が悪いのか? | Cherry Pie Webなど)だが、かすれて見える原因は、ウェイトが細すぎるからではない。

例えば、本文に游ゴシックを使っているWIREDの記事(「癌」という名のドラゴン──父はゲームをつくった。死にゆく息子のために « WIRED.jp)のキャプチャ画像を見てほしい。



WIREDの記事をFirefoxで表示したもののキャプチャ

注目してほしいのは、ただ文字が薄いというわけではなく、線によって濃さが違うということだ。曲線の多い仮名に比べて、ピクセルに沿った直線の多い漢字の方が黒くみえる。

また、アプリケーションによって字のかすれ具合が違う。たとえば、次の画像は游ゴシックを同じウェイト、同じポイント数で、それぞれFirefoxとメモ帳を使って表示したものだ。



Firefoxで表示したもの



メモ帳で表示したもの

どちらも薄くかすれて見えるが、メモ帳の方がより薄く見える。

ガンマ補正

Windowsのフォントレンダリングは、ただ薄いのではなく、線によって濃さが違う。その原因は、ガンマ補正が適切に行われていないことだ。

ガンマ補正とは何かを説明するために、色の仕組みから説明する。

多くの人が知っているとおり、コンピューターでは色を数値で表したものを処理する。色を数値で表す方法には色々あるけれども、いちばん有名なのは、RGBカラーコードだろう。たとえばCSSでは、カラーコードを使って、黒はrgb(0, 0, 0)、白はrgb(255, 255, 255)とも表せる。これは、色を構成するR(赤), G(緑), B(青)の三原色それぞれの輝度を、0から255までの数値で表現したものだ。

しかし、ここから先の話はあまり知られていないことだと思う。多くの人は、rgb(255, 255, 255)の半分の明るさのグレーは、rgb(128, 128, 128)だと思っている。だけど、それは間違いだ。実は、rgb(128, 128, 128)はrgb(255, 255, 255)の2割ほどの明るさしかない暗いグレーであり、本当に半分の明るさになっているグレーはrgb(188, 188, 188)なのである。*1

これには、ブラウン管モニターの特性に関する歴史的事情が関わっているのだけれども、深くは立ち入らない。大事なのは、rgb(128, 128, 128)が半分の明るさだと思ってプログラムを書くと、グレーが暗くなってしまうということだ。だから、暗くなる分を考慮して、明るめの画像を表示しないといけない。そのような補正処理を、「ガンマ補正」と言う。

Windowsのフォントレンダリングは、ガンマ補正が2重、3重に掛かっている

おそらく、Windowsのフォントレンダリングにおいてもガンマ補正が行われているのだが、どういうわけかガンマ補正が多重に掛かっているようだ。つまり、グレーを明るくする処理を重ねたためにグレーが明るくなりすぎている。(rgb(128, 128, 128)を補正してrgb(188, 188, 188)になるはずが、rgb(223, 223, 223)やrgb(240, 240, 240)になっている。)

参考として、GIMPのレベル補正機能を使ってガンマ補正を逆向きに(グレーが暗くなるように)行った画像を載せる。指定した0.46, 0.21というのは、ディスプレイのガンマ値2.2の逆数と、その2乗だ。



Firefoxで表示したものにガンマ補正(0.46)を掛けたもの



メモ帳で表示したものにガンマ補正(0.21)を掛けたもの

字のかすれが解消され、全体が同じ濃さの文字になっているのが分かるだろうか。この結果から、ブラウザーに関してはガンマ補正が2重に、メモ帳に関しては3重に行われていると考えられる。(※このことに関して追記あり)

游ゴシック細字は本来この程度の黒さで表示されるべきなのであり、ただ細すぎるから薄くて読みづらいという話ではない。太くすると読めるようになるのは、グレーの領域が少なく、ガンマ補正が適切でないのが誤魔化されるだけであり、Windowsのフォントレンダリングが適切でないことには違いない。将来的にはガンマ補正を修正して、細い字もムラなくちゃんと表示できるようになるように改善してほしいと思う。

追記


追記2

ブックマークコメントでも意見を頂きましたが、メモ帳のようなGDIでClearTypeを使ってレンダリングしているケースに関しては、ClearTypeの設定によって表示が変わるようです。自分もメイリオ初期設定から変更していたように思います。なので、3重に補正が掛かっているというのは誤解で、たまたまそう見えるガンマ値が設定されていただけであるようです。

*1:色空間がsRGBの場合