LoginSignup
34
32

More than 5 years have passed since last update.

Swiftのクロージャで再帰

Posted at

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

というクロージャは成立します.fInt -> 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_ffとが交互に起動されます(関数の呼び出し回数は2倍になっている).'Advanced Swift'に出てくる例は,これをベースにメモ化に使う辞書も挟み込んで,メモ化フィボナッチ関数を実装しています.これは必見だとおもいます.

不動点コンビネータがHaskellに劣らずにシンプルに記述できるとは思いませんでした.感動しました.
以上です.

34
32
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
32