Lua で不動点コンビネータ

たまには Lua も書かないと忘れてしまいそうなので,久しぶりに Lua を書いてみた.
お題は,最近の日記でも扱った,不動点コンビネータ
散々既出だろうし,新規性は全くないけど,まぁ個人の blog だし その辺りは勘弁してください,ってことで.


さて,知らない人のために説明すると,不動点コンビネータとは,

function fix(f)
  -- 実装
end

local fact = fix(
  function( fact, x )
    return x > 1 and x * fact(x-1) or 1
  end
)

って感じで,ラムダ式再帰を書けるようにする,ちょっと不思議な関数 fix のこと.

勿論,これは local function を使って

local function fact(x)
  return x > 1 and x * fact(x-1) or 1
end

と書けばいいけど*1高階関数*2for 文では,やはりラムダ式のまま書けると便利でしょ,ってこと.


で,実際に書いてると,こんな感じ.

function fix(f)
  local function fixed(...)
    return f( fixed, ... )
  end
  return fixed
end

local fact = fix(
  function( fact, x )
    return x > 1 and x * fact(x-1) or 1
  end
)

特に何も捻ってないので,分かる人なら問題なく分かる筈.


さて,こいつが実際に使い物になるかどうかが気になったので,
竹内関数を使ってベンチマークを取ってみます.

-- 不動点コンビネータ
function fix(f)
  local function fixed(...)
    return f( fixed, ... )
  end
  return fixed
end


-- ベンチマーク関数
do
  local collectgarbage = collectgarbage
  local clock = os.clock
  local print, format = print, string.format

  function benchmark( f, ... )
    -- 予め GC を呼ぶ
    collectgarbage("collect")
    -- 測定
    local t = clock()
    local result = f(...)
    local dt = clock() - t
    -- 表示
    print( format( "%d, %fs", result, dt ) )
  end
end

-- 実際に測定(竹内関数を使う)

-- 普通に再帰で書いた場合
print( "rec. version:" )

local function tarai( x, y, z )
  if x <= y then
    return y
  else
    return tarai( tarai( x-1, y, z ), tarai( y-1, z, x ), tarai( z-1, x, y ) )
  end
end
benchmark( tarai, 12, 6, 0 )

print( "" )


-- 不動点コンビネータ
print( "fix version:" )

benchmark(
  fix(
    function( tarai, x, y, z )
      if x <= y then
        return y
      else
        return tarai( tarai( x-1, y, z ), tarai( y-1, z, x ), tarai( z-1, x, y ) )
      end
    end
  ),
  12, 6, 0
)

print( "" )

ideone.com で走らせてみたら, time limit exceeded と怒られてしまった*3ので,
手元で走らせてみた結果がこちら:

rec. version:
12, 1.638000s

fix version:
12, 3.572000s

流石に再帰で書いたほうが速いが(当たり前),この程度なら気にならない,筈.
なので,気に入ったら使ってみてもいいんじゃないでしょうか.

おまけ

ついでなので,関数に名前を付けられない場合の不動点コンビネータ,すなわち Y コンビネータも書いてみた.
参考: http://d.hatena.ne.jp/willowlet/20091122/1258894948


上の参考ページの例だと,渡す関数がカリー化されている必要があるが,
今回作った fix は,カリー化されてない関数を受け取るものだったので,
折角なので,カリー化された関数を受け取るものと,カリー化されていない関数を受け取るもの,両方を作ることに.


まずカリー化された関数を受け取る版:

Y1 = function(f)
  return (
    function(x)
      return x(x)
    end
  )(
    function(m)
      return f(
        function(a)
          return m(m)(a)
        end
      )
    end
  )
end

tarai1 = Y1(
  function( tarai ) return function(x) return function(y) return function(z)
    if( x <= y ) then
      return y
    else
      return tarai( tarai(x-1)(y)(z) )( tarai(y-1)(z)(x) )( tarai(z-1)(x)(y) )
    end
  end end end end
)

print( tarai1(12)(6)(0) )

…すごく,キモいです.


ので,せめて tarai( 12, 6, 0 ) と書けるようにします:

Y2 = function(f)
  return (
    function(x)
      return x(x)
    end
  )(
    function(m)
      return f(
        function(...)
          return m(m)(...)
        end
      )
    end
  )
end

tarai2 = Y2(
  function( tarai )
    return function( x, y, z )
      if x <= y then
        return y
      else
        return tarai( tarai( x-1, y, z ), tarai( y-1, z, x ), tarai( z-1, x, y ) )
      end
    end
  end
)

print( tarai2( 12, 6, 0 ) )

これなら,まぁ,許容範囲ですかね.


続いて,カリー化されていない関数を受け取る版は,こちら(ちょい変更)

Y3 = function(f)
  return (
    function(x)
      return x(x)
    end
  )(
    function(m)
      return function(...)
        return f( m(m), ... )
      end
    end
  )
end

tarai3 = Y3(
  function( tarai, x, y, z )
    if x <= y then
      return y
    else
      return tarai( tarai( x-1, y, z ), tarai( y-1, z, x ), tarai( z-1, x, y ) )
    end
  end
)

print( tarai3( 12, 6, 0 ) )

m(m)fixed に対応する,ということに気づけば,特に難しくありません.


ともあれ,こんな感じで書けば,不動点コンビネータ自身からも再帰を取り除くことが可能になります.
この辺の話は,なかなか面白いので,興味があれば調べてみるといいんじゃないでしょうか.


あ,言うまでもないですが,これらの tarai1, tarai2, tarai3 は,かなり遅いです.
特に tarai1 の遅さは壊滅的で,実用には全く向きません.
tarai2 なら少しはマシですが,本文で定義した fix と比べるとイマイチですし,
普通に再帰で書いた場合に比べると,その性能差は歴然です.


そうなる理由は,変数束縛を行っていない変数の再束縛が行えないために,本来なら1回作れば良いクロージャを,
関数呼び出しの度に制作しているからで,変数束縛変数の再束縛を解禁すれば,速度的には かなりマシになります.((というか,最終的に,本文で定義した fix と同じになります.))
変数束縛変数の再束縛を使って効率化した具体例は,そろそろ面倒になってきたので,後の機会に回すことにします.


まぁ,要するに,変数や再帰呼び出しは偉大だ,ってことですね.

// ループで書ける場合は,再帰で書かずにループで書けば ずっと高速になる,というのは内緒です.

おまけ2

実のところ, Lua は無名関数の中に関数を定義できるので,

( function()
  local function tarai( x, y, z )
    if x <= y then
      return y
    else
      return tarai( tarai( x-1, y, z ), tarai( y-1, z, x ), tarai( z-1, x, y ) )
    end
  end
  return tarai
end )()

とでも書けば, fix とか使わなくても,再帰関数を式の中で生成できたりします.


これは,普通に再帰関数を定義した場合と比較しても,速度の劣化は殆ど無いですので,
高階関数や for 文に再帰関数を渡したいケースが どの程度あるかは謎ですが,
そのようなケースでは, fix よりむしろ こっちを積極的に使っていくといいと思います.

*1:なお,グローバルに定義する場合は,一旦ローカルで定義してからグローバル変数に代入する,という方法を使うべきであるが,それは今回の本題ではないので,グローバルは考慮しないことにする.

*2:「関数を取る関数」のこと.

*3:http://ideone.com/yOWg0