Haskellでは遅延評価が原則になっているそうな。遅延評価というのは要するに「必要になるまで計算しない」ということ。Haskellでは構文上で明示的に遅延評価を書くことがないので意識されにくいかもしれないが、効率に大きな影響を与える。実際に使ってみると、遅延させるためにはいくらかのオーバーヘッドは発生するが、それ以上に無駄な計算をしている場合が少くないことに気付く。
ところで、SICPの中では遅延評価をストリームと絡めて論じている。これは興味深い観点だ。例えば、一般的によく使うストリームはファイルであろうが、これも遅延の一種と捉えて扱えるのだ。逆に、他のオブジェクトをストリームとして扱えない道理も無いというわけだ。
Schemeではdelay手続で明示的に遅延オブジェクトを作り、force手続で明示的に評価できるようになっている。例えば引数の和を返す手続を遅延させてみよう。
(define (add x . y) (if (null? y) (delay x) (delay (+ x (force (apply add y)))))) (define lazy-obj (add 1 2 3 4)) (force lazy-obj)
addが評価されたときではなく、それが返したオブジェクトをforceに渡したときに実際の評価が始まる。この例では意味があまり無いが、将来的に使う可能性が薄そうな演算を後回しにすることで全体の効率を向上させる役にたつ。
で、Schemeに関心が無いと面白くないと思うので、C++で書いてみた。要はこういうことをやってるんです。
#include <iostream> using namespace std; class add { private: add *callee; int num; add(int num, add* callee) : num(num), callee(callee) {} public: add(int num) : callee(NULL), num(num) {}; operator int(void) { if(callee) num+=*callee; delete callee; callee = NULL; return num; } add& operator()(int num){ return *(new add(num, this)); } ~add() { delete callee; } }; int main(void) { add lazyadd=add(1)(2)(3)(4); cout << lazyadd; return 0; }
結構面倒。どこか間違えてるような気もしないでもない。C++では引数の扱いがそれほど柔軟ではないので呼び出し方も違う。
何より、部分適用に使おうとすると一部のオブジェクトを複数から共有するようになってしまい、メモリの解放時に不整合が生じてしまう。(上の例の中ではそういう使いかたにならないようにしているが、使う側に制約を強いるのは良い設計とは言えない。)私は「開けたら閉める」のメモリ管理を好むが、コードが少し複雑になってくるとメモリ管理まわりのコードは複雑さに拍車をかける結果になっている場合は少なくないようにも思う。それを飼い馴らすのがC++の醍醐味でもあるんだが。