おとうさんにもわかるYコンビネータ!(絵解き解説編)
先日YコンビネータのきしださんのYコンビネータのエントリが話題になっていました。
ずいぶん日にちが経ってしまいましたが、自分も、自分なりにYコンビネータのあたりを絵解きで整理してみたいと思います。きしださんのエントリタイトル*1に引っ掛けて、目標として、自分の父親(非プログラマ。その辺のおっさん)でも解る内容を目指します。
なぜ不動点演算子というのか、不動点だったらなぜ再帰なのか、この辺りも含めて、実感を持って納得できればいいなと思います。
きしださんのエントリのおさらい
本題の前に、きしださんのエントリをおさらいしておきます。
って事が書いてありました。
普通のプログラミング言語では常識である、if文もfor文やwhile文も、仮引数以外の変数・変数代入も、果ては数値すらすべて使わずに、ただただ関数だけでそれらを「エミュレート」できちゃってます。関数というか、正確にはλという単純な文字列変換ルールだけでできちゃってます。
λSUGEEEEEEEEEEEEEEEEEEEEEE!!!!!!!!*2
きしださんのエントリの趣旨は、「λSUGEEEEEEEEEEEEEEEEEEEEEE!!!!!!!!」であって、Yコンビネータはその一部な訳ですが、そのお話の一部であるYコンビネータの所を、今回整理してみまず。絵が汚いのはご愛敬。
以下、本題。
Yから出力される関数の性質
このYが出力する関数(図では☆=Y(M))は、ある性質を持ちます。自分の元ネタであるYに入力した関数(図ではM)に入れると、また自分と同じ関数(図では☆=M(Y(M))=Y(M))が出てくるのです。Mからの出力である☆とMへの入力である☆とが同じという訳ですから、M(Y(M))=Y(M)と言う訳です。
Yから出力される関数の性質ーもういちどMに入れ直してみる
最初にMにY(M)を入力したときの出力はM(Y(M))ですね。それをもう一度Mに入れてます。すると出てくるのはM(M(Y(M)))と言う事になりますが、これもやはり同じく☆であり、Yから出てきたときの☆=Y(M)と同じです。つまり、M(M(Y(M)))=Y(M)と言う訳です。
Yから出力される関数の性質ー何度も何度もMに入れ直してみる
何遍やっても変わりませんね。M(M(M(M(M...M(Y(M)))...)))) = Y(M)です。Mにとって、Y(M)という入力は、変わらずにそのまま出てきてしまう特別な入力なのです。この特別な入力を不動点と言うらしいです。
…で? それと再帰/ループと何の関係があんの?
ですよねー。何回もMを適用(呼び出し)してるあたりがちょっと関係ありそうですけど。きしださんのエントリだと、ここが僕には解りずらかった。で、考えてみました。
もともと、再帰/ループがやりたかった
中学校で習った1次関数
は、にを入力すると、を計算した結果が出力されます。
再帰というのは、関数が自分自身を使ってる事です。この関数は再帰してません。でも例えば、それをちょっといじった、こんな関数にしてみます。
自分()で自分()を使っています。これが再帰です。
ちょっといじって、じゃなくての所を入力とする、別の関数を作ってみる
ここで、じゃなくてむしろの所を入力として受け取るようにいじった、別の関数を作ってみます。
この関数は、関数を入力として受け取ります。そして、その関数を使う新しい関数を作って出力します。入力も出力も関数です。とくに再帰もしてないです。入力として関数を受け取ると、のの所にその関数を当てはめた結果が出力されます。
例えば、入力がという関数だったら、
つまりという関数が出力されます。もちろん、この出力された関数は普通に使う事が出来る普通の関数です。例えば、1を入力したら7が出力されます。
この関数をと呼ぶ事にしましょう。
にを入力してみよう
は、関数を入力として受け取ります。ここで、もともとの関数を入力してみましょう。
すると、出力されるのは、。入力であるとまったく同じです。
関数には、いろいろな関数を入力できますが、関数にとっては特別な入力ですね。
はの不動点だ
は、そのままでも、に入力した結果でも、まったく変わりません。このような特別な入力を、不動点というらしいです。
これを式で書くと、
(「はの不動点である」と言っている式)
です。
の実体はだったので、上記の式の右辺は(にを当てはめた)です。
つまり、上の式は、
(「はの不動点である」と言っている式2)
と同じ事です。これは、上での内容を決めたときの式とまったく同じです。が、自分()で自分()を使っている=再帰する関数であるという事を、式で書いたもの、そのものずばりです。
つまり、どういう事かというと、
「はの不動点である」という事が、
という事です。
じゃあ上半分は?
実例で見た説明を当てはめると、最初の方の図はこうなります。
Yと呼ばれるある関数は、入力された関数(例:)の不動点(例:)を出力します。不動点を出力するので、Yは、不動点演算子であるとか、Fixed point combinatorであるとかいうらしいです。Yの出力は不動点なので、Yに入力した関数(例:)にそれ(例:)を入力すると、そっくりそのままそれ(例:)が出力される、と言う訳です。
Yに入力するのは何でもいい訳じゃなくて、「関数を入力として受け取り、その関数を使う新しい関数を作って出力する関数(入力も出力も関数)」です(もちろんもそうなってました)。そうするとYから、そいつの「不動点である関数」=「再帰する関数」が出力されてきて、関数だけで再帰が出来るという訳です。めでたしめでたし。
Yコンビネータは、使い道がない?
Yコンビネータの一番偉いところは、そのおかげで、関数(正確にはλという単純な文字列変換ルール)だけでプログラムがすべて表現できるという事だと思います。それによって、プログラミングに関するいろんな原理の研究に数学のスーパーパワーを使いやすくなります。数学のスーパーパワーを使うと、完全に正しいと保証されたプログラミングに関する技術が産み出せます。その辺のさわりを、きしださんが解説されています。
きしださんは、
結論をいえばYコンビネータには、なにかの処理を便利にする能力はない。関数であらゆる計算ができるということが示せれば、あとは用なしだ。理論の礎としてうまってしまえばいい。
http://d.hatena.ne.jp/nowokay/20090413#1239617061
といいます。たしかにYコンビネータ自身は、結局のところ再帰を産み出してくれるだけです。単なる再帰なら、実際のプログラミングではYコンビネータなんて使わなくても出来ます。
じゃあ、Yコンビネータとか不動点とかは、偉い学者さんとかが研究に使えばいいもので、普通のプログラマには何の意味もないモノなのでしょうか?
さあ、Yコンビネータ(不動点演算子)を使おう!
意味がない事はないと思います。Yコンビネータとか不動点とかで出てくる考え方は、理論だけじゃなく、実際のプログラミングでも応用できたりもします。ちょっとそれを見てみましょう。
というわけで、Haskellやろうぜ!(おまけ)
いきなりですが、Haskellの宣伝です。
ぬぬ、スタックオーバーフロー。
これは、式がそのまま評価されてしまうことに問題があるようです。そこで遅延評価するための変換をいれてみます。
http://d.hatena.ne.jp/nowokay/20090409#1239268405
ぬぬ、またしてもオーバーフロー
これは、boolで真の場合も偽の場合の式もその場で評価されてしまって無限ループになってしまうためで、やっぱり遅延評価しないといけません。
http://d.hatena.ne.jp/nowokay/20090409#1239268405
きしださんのエントリで、普通のプログラミング言語の規則である評価順番が小さな躓きポイントとして挙げられていました。ただでも込み入った事を考えているのに、こういった躓きポイントは痛いです。それに、それを回避するために、本当は「正しい」はずのコードをもう一捻りして書き換えてやらなくてはいけないのも、めんどくさいしすっきりしません。
でも、Haskellなら大丈夫。デフォルトで遅延評価だからです。必要なところを必要なだけ評価していくので、このような問題は起きません。単純に正しく書いておけば、きちんと動くプログラムになってくれます。
さらに、Yコンビネータというか不動点演算子をこんなにすっきり書く事が出来ます。
y(f) = f(y(f))
このコードは、上で書いた
(「はの不動点である」と言っている式)
に対して、
(「はにを入力した結果である(上の最後の図の上半分)」と言ってる式)
を当てはめた式、
(""を""に書き換えた式)
そのものです。大文字と小文字が違うだけで、プログラムとこの式はまったく同じですね。定義をそのまま正しく書いてやるだけで、ちゃんと動くプログラムになってます。評価順とか関係ないです。すごいですHaskell。
WebにHaskellの情報は結構ありますが、玄人向けです。「やさしいHaskell入門(A Gentle Introduction to Haskell)」というドキュメントがありますが、これは「天才な人にはやさしい」という意味なので気を付けて下さい。ふつうの人には、この本がふつうの意味でやさしいくていい感じです。興味が湧いた人は、まずこの本を読む事をお勧めします。