Island Life

< parameterizeの面倒くさい話 | トランプのヤバさ >

2016/12/22

call/ccはいつ使う?

(この記事はLisp Advent Calendar 2016の23日めの記事です)

昔のTVは、取扱説明書に回路図がついていた。 中学校に上がるか上がらないかの頃、多少電子工作をかじって背伸びしていた私は 家のTVの回路図を眺め、 「ここを映像信号が通ってるはずだから、この信号を取り出してカセットテープに録音したり 再生した信号をここに流し込んだらビデオになるんじゃないか」とか無茶なことを 考えて試したことがある。まだ周波数帯域という概念を知らなかったんだな。


さて。第一級継続とそれを取り出す手続きcall/ccは、 Schemeの特徴としてよく挙げられる。 使い始めの頃は、よくわかんないけどなんだかすごそうな機能に見えて憧れたりする。 いくつかcall/ccを使った例を実装してみるうちに、 安全なsetjmp/longjmpみたいなもんにすぎない、と納得できるんだけど、 いざこの機能をどう使えば良いか、となると難しい。

普段のプログラミングで使えそうな場面は、まさにsetjmp/longjmp相当の、 関数呼び出しの深いところから脱出する時くらいのものだろう。

例えばハッシュテーブルのすべてのキーとその値に与えられた手続きを適用する hash-table-for-each (refj:hash-table-for-each) がある時、 「述語predを満たすキーと値を見つけたら捜索を打ちきってreturnする」 なんてのが書きたいとする。 Schemeにはreturnが無い(「WiLiKi:Scheme:なぜSchemeにはreturnが無いのか」)から、 こういう時は継続の出番だ。 (処理系にlet/cc (refj:let/cc)などの簡略化マクロがあれば、 call/ccよりそっちを使った方が見やすいだろう)

(define (hash-table-any table pred)
  (let/cc return
    (hash-table-for-each table  ; tableを順次調べ、predが真を返したらreturn
       (lambda (key val) (let ((r (pred key val)) (if r (return r))))))
    #f))  ; 見つからなかったら#f

しかしこれ以外の応用となるとどうだろう。 良く例外処理だのコルーチンだの非決定性計算だのが挙げられるけれど、 普通のアプリケーションプログラミングをしていてこういった制御構造を「自分で」 書く必要に迫られることは滅多にない。というより、むしろこれらの機能を 処理系が提供しているなら、自分で書くのは避けた方が良い。

例えば例外処理なら、最近のSchemeにはguardがあるはずだ。 自分でcall/ccを使って例外処理を書いてしまうと、 組み込みのguardを使った他のライブラリと整合性が取れなくなる可能性がある。

コルーチンにしても、目的に特化したものなら例えば gauche.generatorモジュール(refj:generate)、あるいはSRFI:121(refj:make-coroutine-generator)等がある。 これらは内部的には継続を使っているんだけれども、 アプリケーション層でそれを気にする必要はない。

プログラミング言語のユーザに、「言語を使ってアプリケーションを書く人」と 「言語仕様を決めたり実装したりする人」という区別があるとしたら、 call/cc は「アプリケーションを書く人」にとっては ほぼ使う必要がない機能と言ってしまってもいいかもしれない。


もちろん例外処理やコルーチンライブラリを「実装する人」はcall/ccを使うんだけれども、 もしこれが「一つの処理系によって仕様が決められるような言語」であれば、 call/ccみたいな面倒くさい機能をわざわざ 仕様にする意味はあまりない。 新たな制御機能のアイディアを思いついたら、処理系のソースを直接いじって実装して パッチを投げればいいからだ。

call/ccは、原理は単純なんだけれども、 現実の実装に課す制約が多い機能でもある。特に関数のcall/returnを主流の言語の実行モデルに 合わせたい場合に色々面倒が多い。既存の言語の多くが、やろうと思えばcall/ccを 提供できるとしても、それをやらずに目的の制御構造を直接実装してしまう理由のひとつは それだろう。実装を複雑にしてまでcall/ccを実装するメリットが見えないというわけだ。

それでもSchemeがあくまで call/cc にこだわるのは、 別にそれが伝統だからとかアカデミックに重要だからとかかっこいいから、ではない。


プログラミング言語は、コンピュータで実行するプログラムを書く言語であると同時に、 プログラムについてのアイディアを人とやりとりするための言語でもある。 アルゴリズムを説明するのに、全て自然言語で説明するのはあまりにまどろっこしく 不正確だ。擬似コードでも良いが、ディテイルまで 詰めたければ明確な定義のある言語で書くのが結局は一番手っ取り早かったりする。

そのアイディアの説明であるコードが、直接実行できて動作を確かめられるなら、なお良い。

call/ccは、新たな制御構造のアイディアを議論する際に、 直接制御の流れをいじれて、かつ比較的明確な定義があるプリミティブ、として活躍するのである。

R6RS以降標準となった例外機構のguardは当初SRFI:34で議論された。 その参照実装を見ると、call-with-current-continuationがネストしていて 慣れないといかにも恐ろしげに見える。 でも慣れるとむしろどのコードがどの動的環境で実行されるかが明確に示されているので、 なまじ曖昧に自然言語で説明されるよりもわかりやすいのである。 (なお、実はGaucheのguardにはknown bugがあって srfi-34と完全に同等になっていない… 効率を落とさずにサポートするのが今の実装では面倒なので先延ばしにしている。)

部分継続(refj:gauche.partcont)みたいなトリッキーな制御構造も、効率を考えなければcall/ccで実装できて、それは意味を考える上でも、実装の際の動作確認の比較対象としても、とても有用だ。

このように、「言語の仕様を考えたり実装したりする人」にとって、call/ccというのは新たな制御構造を試すのになかなかに便利なツールなのである。 (ただし、トップレベルの継続のように仕様が曖昧な部分も無くはない。)


先に、プログラミング言語のユーザに、「言語を使ってアプリケーションを書く人」と 「言語仕様を決めたり実装したりする人」という区別があるとしたら、と書いた。 けれども、そういった区別は必要だろうか。

Lisp系言語は、"Programmable programming language" と呼ばれてきた。 その言語を使ってプログラムを書く、だけではなく、 その言語を使って言語自身をいじるための言語でもあるということだ。

もちろん、言語仕様は誰かが便利に決めてくれればいい、それを使って アプリケーションを作ることに関心がある、というユーザもいていい。 でも、「なんかこのパターン毎回書いてるけど言語組み込みになってたら便利じゃない?」とか 「今はこういう仕様になってるけどコーナーケースはこう動くべきじゃ?」とか 「やっぱり言語にはGOTOが必要だ!」とか 思いついちゃった時に、

  • そのアイディアをコードとして明確に書き下して、
  • 自分で実行して確かめられて、
  • さらに他の人とシェアして別の処理系で試してもらったり新しいアイディアをもらったりする

ってことが出来たら、それはそれで楽しい。そうやって練ったアイディアがいずれSRFIに、そしてRnRSへと反映されてゆくかもしれない。


昔のTVについていた回路図は、ほとんどのユーザにとっては無用の長物だったろう。 けれども町の電気屋さんは持ち込まれた近所の家のTVを直すのに重宝しただろうし、 腕に覚えのあるお兄さんは魔改造するのに使ったかもしれないし、 電子工作少年の心をくすぐるのにも十分だった。 その気になれば、中身に手を突っ込んでいじれる、そのための共通のコトバとして、 call/ccをとらえてみるといいかもしれない。

Tags: Programming, Scheme, call/cc

Post a comment

Name: