CPS関数をループ実行する
CPS(Continuation-Passing Style)の関数をループ実行するのに使えるユーティリティモジュールを試してみた。
今回試したのは、
- CPS (v0.18) http://search.cpan.org/~pevans/CPS-0.18/lib/CPS.pm
- Async::Defer (v0.9.4) http://search.cpan.org/~powerman/Async-Defer-0.9.4/lib/Async/Defer.pm
- AnyEvent::Tools (v0.12) http://search.cpan.org/~unera/AnyEvent-Tools-0.12/lib/AnyEvent/Tools.pm
なお、ここではCPS関数とは単に「結果をコールバック呼び出しで返す関数」全般と考えている。もしかするとこれって、厳密にはCPSとは言わないのかもしれない。
コードはこんな感じ。
結果:
--- sync, CPS::kloop number: 1000, stack_num: 6 --- sync, Async::Defer Deep recursion on subroutine "Async::Defer::done" at ./cps_loop.pl line 141. Deep recursion on anonymous subroutine at /usr/local/share/perl/5.14.2/Async/Defer.pm line 342. Deep recursion on subroutine "main::sync_cps_add" at ./cps_loop.pl line 142. Deep recursion on subroutine "Async::Defer::continue" at /usr/local/share/perl/5.14.2/Async/Defer.pm line 398. number: 1000, stack_num: 6007 --- sync, AnyEvent::Tools::async_repeat number: 1000, stack_num: 8 --- async, CPS::kloop number: 1000, stack_num: 11 --- async, Async::Defer number: 1000, stack_num: 13 --- async, AnyEvent::Tools::async_repeat number: 1000, stack_num: 8
CPS関数では、コールバックがCPS関数内で同期的に呼ばれる場合と、AnyEventなどを使ってCPS関数外で非同期的に呼ばれる場合がある。同期的に呼ばれる場合、注意しないと関数の再帰呼び出しがループ回数に比例して繰り返されてしまう危険性がある。
上の例では同期型CPS関数と非同期型CPS関数を用意し、1000回ループさせている。
CPSモジュールでは、内部でtail callをうまく使っているので同期型CPS関数をループさせてもコールスタックが深くならないのが素晴らしい。
Async::Deferモジュールは非同期型関数を前提に使われているので、同期型CPS関数をループさせるとコールスタックが深くなってしまう。今回はuse warningsしているので警告が出ている。
AnyEvent::ToolsモジュールはAnyEventに依存していて、コールバックの実行はAnyEventに任せているので、同期型CPS関数でもコールスタックが深くならないのだと思う。そのかわり、同期型CPS関数をループさせる場合でもどこかでAnyEventのcondition variableをrecvしないといけない。