2010年2月28日日曜日

call/ccの冒険~その1

プログラミング言語に於ける抽象化、と言うのはどう言う意味でしょうか。
抽象……。まあ、一般用語で言うと「ボンヤリとして良く分からない曖昧模糊」と言う印象があるのでは。
例えば、キャバ嬢が言う

「あのお客さんの話って抽象的で良く分かんないのよね~~。」

みたいな感じが用法でしょうか。まあ、確かにキャバ嬢相手に抽象論をするお客さんはサイテーだと思います(笑)。気をつけましょう(笑)。分かりやすい話をせんと。

とまあ、実際問題、一般生活の感覚で言うと、「抽象性=難解」と言うニュアンスが含まれますね。「抽象的な話はどーでもいいんだ!具体的な話をしろ!」みたいに怒られる事もしばしばです。

それはさておき。

僕はフォーマルでは全くCS(Computer Science)を学んだ事が無いわけなんですけれども。
一応、僕の感覚から言うと「プログラミング言語の抽象化」と言うのは「プログラミング言語を分かりやすくする」と言うのと同義です。一般文脈の「抽象」とは丸で逆ですね。
この抽象、ってのはどのレベルから見て抽象なのか?と言うと、我々人間側から見て、ではないと思います。コンピュータハードウェアから見て、って事でしょう。逆に言うと、ハードウェアレベルでの「具体」と言うのは0と1とで構成されたバイナリ世界の事です。そして人間の思考様式に近いレベルになればなるほど「抽象」レベルが上がってくるわけです。

プログラミング言語の進化、と言うのは必ずしもCOBOL的な話ではなくって、「人間の思考形式に近づいてくる」と同義だと思います。如何に人間に分かりやすいレベルまであげてくるのか。当然ですよね。そうじゃないと「使い辛い」と言う事だから、です。
考えてみると、プログラミング言語ってのは元々「分かり辛い」ものなんです。いまだに人間が「使いやすい」ツールにはなってない。なってないからこそ他人が書いたコードは読みづらい(笑)。書くのも大変。だから色々な小手先の改良を加えられてきてる。それらはみんな「抽象化」です。

ところで、プログラミング言語設計上での色んな試み、つまり、「人間になるべく分かりやすい」設計をしよう、と頑張ってきた歴史があるわけですけど。当然人間が作るものなんで、抽象度が明後日の方向に行っちゃってるものがある。
こう言うのは「難解なものを理解したい」と言う挑戦欲は刺激しますが、一方、ツールの開発、って観点から見ると単に「失敗した」って場合もあるとは思うんですよね。「人間に分かりやすく」しようとして設計したのにそうじゃない。こう言う場合は責められるのは学んでる側じゃない。あくまで、「設計が」責められないとならないんです。本来ならね。

Schemeで悪名高いcall/cc。これは本当に悩ましいです。ある意味抽象化の失敗例としては顕著な例じゃないか、とも思えます。これを理解するのに非常に苦しみます。何だか良く分からん。
ここでは仕様書に明言されてる「脱出」に焦点を絞ってcall/ccに付いて考えてみたいと思います。

ではここで非常につまんない例題を考えてみます。

1からnまでのすべての整数を足し合わせる手続きsumを定義せよ。

多分Schemeを勉強した人では鼻歌まじりに次のようなコードを書き上げるでしょう。

(define (sum n)
(let loop ((count 1) (result 0))
(if (> count n)
result
(loop (+ 1 count) (+ count result)))))

letrec使う人もいるでしょうし、あるいはinternal defineを使う人もいるやもしれません。
いずれにせよ、基本的な繰り返しの問題ですし、Schemeでは当然の如く「再帰」で記述している筈です。基本、これ以外に手がない。
Schemeの場合は何でも再帰、って皮肉られてたりしますし、繰り返し構文であるdoで書いたとしても、結局内部では再帰に変換されてしまう。繰り返し専用の制御構造ってのが存在しないのです。

表向きは。

実は、call/ccを使うと、Schemeでも無理くり反復を行う事が出来るんです。極めて手続き型言語的発想で。
次がそのコードです。

(define (sum n)
(let ((tag #f))
(let ((count 1) (result 1))
(call/cc
(lambda (body)
(set! tag body)))
(call/cc
(lambda (return)
(and (= count n) (return result))
(set! count (+ 1 count))
(set! result (+ count result))
(tag '()))))))

多分、これ見たとき

「何じゃこりゃ!汚ねえ!」

とか思うと思います(笑)。書いた僕でさえそう思うんですから(笑)。
しかし、これは手続き型言語で言う「反復」なんです。Schemeの繰り返しなのに全く再帰の「さ」の字も出てきてません。嘘みたいでしょうが、ホントです。
しかも、これはキチンと動きます。

> (sum 5)
15
>

上記のコードは、ぶっちゃけた話、構造化プログラミング以前のgoto文での反復を行っています。つまり、call/ccは古典的なgoto文である、と言うのを、それこそ抽象論じゃなく、具体的に説明しているコードです。
なお、ポイントを二つ挙げると、

  1. 一つの手続きに何個でもcall/ccはぶち込んでよい。

  2. call/ccによる脱出とは単純にgotoの事である。


と言う事が見て取れます。

コメント付きで上のコードを再録してみますか。

(define (sum n)
(let ((tag #f)) ;タグを設定
(let ((count 1) (result 1)) ;初期条件
(call/cc
(lambda (body)
;; 継続1。bodyはここのcall/ccの「外側」をクロージャとして表現したもの。
;; 具体的には上部のletから末尾までが範囲となっている。
;; (正確にはλ式。つまり、letの束縛された数値は捕捉対象外である。)
;; tagはそのまた外側なので継続1には捕捉されない。
(set! tag body))) ;そのbodyをtagに代入している。
(call/cc
(lambda (return)
;; 継続2。ここの継続は継続1内に捕捉されている事に注意。
;; 継続1の「外側」に存在しているからである。
;; 条件を満たした場合、継続2はresultを持って計算を脱出する。
(and (= count n) (return result))
(set! count (+ 1 count)) ;手続的計算
(set! result (+ count result))
;; tagへ飛ぶ。tagは事実上一引数関数になってるのでダミー引数'()を渡してる。
(tag '()))))))

一応コメントで解説試みてみたんですけど。詳細は次回(があれば)説明してみますか。
ただ、敢えて言うと、これは全く「関数型」のプログラミングではないです。
そして、call/ccの本質は「関数型言語の要素ではない」って部分なんです。Schemeの機能の中ではあらゆる意味で異質な存在だと思います。
上のコードを理解する為の一つヒントを言うと、(call/cc (lambda ...))と言う形式に惑わされない、って事でしょうかね。Fortran的な意味で言っても(もっとも僕はFortran知りませんが)、単なるgotoなんです。それがcall/ccの「正体」です。

0 件のコメント:

コメントを投稿