Gauche:組み込み関数の再定義
発端 - car, cdrを再定義したら…
今、car/cdr を lisp の様に空リストに対して適用可能にしたとしよう。
gosh> (define (car lis) (if (null? lis) () (car lis))) gosh> (car ()) () gosh> (car '(3)) 3 gosh> (define (cdr lis) (if (null? lis) () (cdr lis))) cdr gosh> (cdr ()) () gosh> (cdr '(1 2 3)) (2 3)
しかし、これ書き下ろしている最中に間違っていると思ってて、 うまく動作することの方に驚いた。 事実、上記の car/cdr を atama/sippo で行うと無限ループに落ちる。
また、 guile では car/cdr でもやはり無限ループに落ちる。 おそらく (define (foo args) body) とあって、 foo のスロットを作る前に body 実体を作ってから foo に束縛するか foo のスロットを作ってから body 実体を作るかの差かなとも思うのだが如何 ?
ちなみに以下が確実なものと考える。
gosh> (define (atama lis) (if (null? lis) () (car lis))) atama gosh> (atama ()) () gosh> (atama '(3)) 3 gosh> (define (sippo lis) (if (null? lis) () (cdr lis))) sippo gosh> (sippo ()) () gosh> (sippo '(1 2 3)) (2 3)
理由
Shiro: あっほんとだ。これはGaucheの方の問題と考えるべきです。 body実体を作るタイミングというか、正確にはbodyがコンパイルされるタイミング の問題です。Gaucheはコンパイル時にいくつかの組み込み関数が 再定義されていないかどうかを調べ、再定義されていない場合は インライン展開するのですが、ご指摘の通りcarが再束縛される前に 本体がコンパイルされるので、本体内のcarは組み込みのcarとして インライン展開されてしまいます。インライン展開しない組み込み関数なら、 再定義すればbody内でも再定義した関数が参照されます。
組み込み関数を再定義する場合は、別名で定義しといて 最後に(define car my-car)とかするのがイディオムですね。
Gaucheに限って言えば、再定義した組み込み関数の中でオリジナルの 組み込み関数を参照するには次のような手があります。
(define car (let ((orig-car (with-module gauche car))) (lambda (lis) (if (null? lis) '() (orig-car lis)))))
別解
これじゃいかんの?
(define car (let ((orig-car car)) (lambda (lis) (if (null? lis) '() (orig-car lis)))))
私もこれでいいのかなぁって思いました。 で、ここで逆に本当に組み込み関数についてはこれでよいか?という疑問がふつふつと。
Shiro: これもよく使われるイディオムなんですが、インタラクティブな 開発において、こう書いてあるファイルを何度もloadして使ってると orig-carの参照が多段になっていくのが個人的にちょっと。動くのは動くんで いいんですが。
でもなあ…
組み込み関数の再定義については gosh では、上記の様な使い方をするようにという仕様(上記でShiroさんがイディオムと表現していることを指してます。誤解してたらごめんなさい。)でもいいのですが、ユーザとしては組み込みか否かはあまり意識したくないような感じ。 ある意味予約された語彙は予約されてない語彙と区別して使わないとユーザの意図した通りにはならないわけですよね。
まぁ大体組み込まれているものは認知されてて(実際私も知った上でやったわけだし)、あまり再定義みたいなことって現実問題としてはやらないのかもしれませんけど、なんとなく気になったりします。cut-sea:2004/01/12 18:06:31 PST
問題の根っ子
Shiro: 最も基本的な関数でさえ再定義できることが Scheme/Lispのいいところだと思っているので、あまり「組み込み関数だから」 特別扱いはしたくないですね。
今回の再帰定義に関していえば、問題は「組み込み関数だから」 ではありません。問題の本質は次の2点にあります。
- インライン展開される関数であること: すなわち、実行時ではなく コンパイル時の束縛が意味を持つこと。(従って、define-macro によるマクロの再定義でも全く同様の問題が生じる)
- トップレベルでの再定義であること: 複数のトップレベル定義がある 場合の動作は明確に定義されていない。特に、 「トップレベル束縛が先か、本体のコンパイルが先か」で結果が違って来る ような場合に困ったことになります。
これらの問題を回避しようとすると、インタラクティブな定義は あきらめて、replでは評価のみ、定義は常にファイルから読む、というような 方向しかないんじゃないか、という気がします。
Generic関数を使ったら?
(事実上)再定義する時にgeneric関数を使うと楽かな...と思って試みたのですが
(define-method + (a b) "Hi") (+ 1 2) ==>"Hi" (methodが適用されている) (+ 1 2 3) ==>6 (素の手続きが適用されている)
+は通るんですけど
(define-method log (a b) "Hi") (log 1 2) ==> "Hi" (log 1) ==> *** ERROR: no applicable method for #<generic log (1)> with arguments (1)
logだと通りません。 generic関数が素の手続きを遮蔽する/しないって決まっていないんですか? もちろん、明示的に
(define-method log (z) ((with-module gauche log) z))
を追加すればエラーは消えるんですが、簡便さのためということで。。。
Shiro (2005/04/15 17:57:06 PDT): これはautoloadとdefine-methodの絡んだバグですね… logはgoshの起動直後はautoloadされる関数として登録されているんですが、 define-methodが同名のgeneric functionを探す際にautoloadを見つけられないので 新たにlogというgeneric funcitonを作って束縛をシャドウしてしまうのです。 define-methodの前に一回でもlogを使っていれば予想通り動作します。
この問題はGauche:GenericFunctionとModuleとも絡むので、 すっきりした解決法が欲しいところです。