WWDC 2014でのスライド'Advanced Swift'に不動点コンビネータを使った見事な方法が載っていたので問題を簡潔にしてメモしておきます.情報をくれた@Ushio@githubさんに感謝.
let
だけでは無理
let
を(letrec
のように)使って
let fib = {(n: Int) -> Int in
return n < 2 ? n : fib(n - 1) + fib(n - 2)
}
と記述しても,
error: variable used within its own initial value
とコンパイラに怒られてしまいます.
素直にクロージャで再帰する記述法はなさそうに思えます.
不動点コンビネータをかませて再帰を実現
上の方法では,クロージャ内部での変数参照の仕方が問題となっています.
ならば,その変数をパラメータとして与えてしまえばいいのでは?ということで,
let fib = {(f: Int -> Int, n: Int) -> Int in
return n < 2 ? n : f(n - 1) + f(n - 2)
}
というクロージャは成立します.f
をInt -> Int
な引数に仕立て上げていますので何の問題もありません(この引数こそが再帰関数そのものなわけですが).
しかし,当たり前ですがfib
は引数が2つ必要な関数になってしまってfib(5)
などとは書けません.なんとか(Int -> Int, Int) -> Int
なクロージャでからInt -> Int
なクロージャへ変換したいところです.
その変換関数の雰囲気は次のような感じ:
func fix(f: (Int -> Int) -> Int, Int) -> (Int -> Int) {/*...*/}
この関数を間にかませて
let fib = fix {(f: Int -> Int, n: Int) -> Int in
return n < 2 ? n : f(n - 1) + f(n - 2)
}
とやればもれなくfib
ができあがるわけですね.
と,ここまでくると,この手のインターフェースはどこか見たことあるかもしれません.不動点コンビネータというやつでした.思い出してきました.きちんとした理屈は専門書に譲るとして,簡単に言うと,
f(fix(f)) = fix(f)
という等式が成り立つような関数のことです.
これが成り立つので,f(...(f(f(fix(f))) = fix(f)
と何回f
を施しても変わらないという面白さ.このグルグルを再帰に応用するんですね.関数の実装の方はこうなっています:
func fix(f: (Int -> Int, Int) -> Int) -> (Int -> Int) {
var fix_f: (Int -> Int)!
fix_f = {(n: Int) -> Int in return f(fix_f, n)}
return fix_f
変数fix_f
は,上のfix(f)
を表現しています.実装の中身も上の等式とだいたい同じ雰囲気になってますね(fix_f
は初期化しておかないとf
の引数にできないため,アンラップオプショナル!
でnil扱いにしておきます).
面白いですね.fix_f
が常に間に挟まっているためfix_f
とf
とが交互に起動されます(関数の呼び出し回数は2倍になっている).'Advanced Swift'に出てくる例は,これをベースにメモ化に使う辞書も挟み込んで,メモ化フィボナッチ関数を実装しています.これは必見だとおもいます.
不動点コンビネータがHaskellに劣らずにシンプルに記述できるとは思いませんでした.感動しました.
以上です.