Y combinator

最近は残業と休日出勤ばかりでうんざりの KrdLab です.
今回はちょっと毛色の違う話題を.内容はいつの間にか自分用のまとめになってしまいましたが...

Y combinator とは?

不動点コンビネータの一種.
不動点とは,ある関数 F を定義したとき,F(p) = p となる p のことを指す.ここで p を関数にまで拡張して考えてみる.すなわち関数 F は高階関数となる.この関数 F の不動点 p を返すような関数 g を考えると,p = g(F) の様に表すことができる.このとき関数 g を不動点コンビネータと呼ぶ.

特にラムダ計算では,

g := λf. (λx. f(x x)) (λx. f(x x))

と表される Y combinator がよく知られている.

なお,combinator については Combinatory logic - Wikipedia, the free encyclopedia にて以下のように説明されている.

A combinator is a higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. [引用]

combinator は,自身の定義に自由識別子 (自由変数と呼ばれる場合もある) が含まれていないため,実行時の環境に因らず,引数 (束縛された識別子) のみから自身の出力が決まる.

性質

不動点の関係を表しているのだから,g(F) = YF = p,かつ F(p) = p より,F(YF) = YF となるはず.実際に適用を進めると,確かにその性質が認められる.

YF = (λf. (λx. f(x x)) (λx. f(x x)))F
   =   (λx. F(x x)) (λx. F(x x))
   = F((λx. F(x x)) (λx. F(x x)))
   = F(YF)

さらに適用を続けると,以下のように再帰構造が現れる.

   = F(F(YF))
   = F(F(F...

つまり不動点 YF は,「関数を引数にとって関数を返す」関数 F による再帰を表しているとも解釈できる.

何ができるのか?

ラムダ式で再帰を表現することができる (つまり繰り返しを表現できる).このような再帰を無名再帰と呼ぶ.
ここでは階乗計算を例に説明する.プログラムでフツーに書けば以下のようになる.

fact 0       = 1
fact (n + 1) = (n + 1) * fact n

これをラムダ式のみで表すために Y を用いる.前述の性質から,「F の繰り返し適用 (再帰) の結果得られる関数」を表す YF に n を渡したものが,fact n と等しくなるような F を定義すればよい.

イメージとしては fact(n) := YFn = F(YF)n より,F は関数と数値を引数にとればよいことがわかる.

コードで書くと以下のような感じになる (大文字始まりなのは気にしないでください).

F = \f n -> case n of
              0       -> 1
              (m + 1) -> n * f m

上記 F に Y を適用することで引数 f は YF となり,再帰構造が生まれる.

よって,目的の fact は以下のようになる (大文字始まりなのは気にしないでください).

fact = Y F
--       ↓
       Y (\f n -> case n of
                    0       -> 1
                    (m + 1) -> n * f m)


実際に引数を与えると以下のように関数適用が進み,階乗計算が実現される (ラムダ式は長いので便宜上 F と表記しています).

(Y F) 3 -> F (Y F) 3
        -> 3 * (Y F) 2
        -> 3 * (F (Y F) 2)
        -> 3 * (2 * (Y F) 1)
        -> 3 * (2 * (F (Y F) 1))
        -> 3 * (2 * (1 * (Y F) 0))
        -> 3 * (2 * (1 * (F (Y F) 0)))
        -> 3 * (2 * (1 * (1)))
        -> 6

以上により,Y combinator を用いることで無名再帰を実現できたことがわかる.

補足

haskell で Y combinator を定義する方法は,ググるといくつか紹介されています.ちなみに fix = \f -> f (fix f) は反則 (?) なので悪しからず.
また,正格評価を基礎とする言語で Y combinator を記述した場合,展開が無限に進んでエラーとなってしまいます.こちらについてはここここで対応版が紹介されています.