動機
C で言うところの static なローカル変数を Scheme (Guile) でやろうとする試みの記事を読んで、私の直観では目的に対してコードが複雑すぎるように思えた。 最初にそう思ったのは全くの直観だったのだが精査してみると変数をハッシュテーブルに格納しようとしているのが複雑さの根源で、 static-let の本文のほうをクロージャとして格納すれば単純化できるのではないかと思い至った。
R6RS は規格の成立を急ぎ過ぎたという批判もあり、色々と機能が行き届かない部分はある。 マクロが使用された場所を区別するのが困難なことについては構文オブジェクトをキーとして使うという案によってどうにか出来そうなのでこれも使うことにする。
実装
#!r6rs
(library (static)
(export static-let)
(import (rnrs))
(define cache (make-hashtable (lambda(x)0) free-identifier=?))
(define-syntax static-let
(syntax-rules ()
((_ ((var init) ...) body body* ...)
(let* ((key (let-syntax ((foo (syntax-rules()))) #'foo))
(closure (hashtable-ref cache key #f)))
(unless closure
(set! closure (let ((var init) ...)
(lambda()body body* ...)))
(hashtable-set! cache key closure))
(closure)))))
)
テストケース
#!r6rs
(import (rnrs)
(static))
(define (counter)
(static-let ((n 0))
(set! n (+ 1 n))
n))
(define (fibonacci)
(static-let ((n 0) ; この n はもちろん counter の n とは別の存在
(m 1)) ; 複数の変数を記述することも出来る
(let ((temp n))
(set! n m)
(set! m (+ m temp))
temp)))
(do ((i 0 (+ i 1)))
((>= i 10))
(display (counter))
(display ":")
(display (fibonacci))
(newline))
想定通りであればこのテストケースは
1:0
2:1
3:1
4:2
5:3
6:5
7:8
8:13
9:21
10:34
という出力を得られるはずだ。
テスト結果
構文オブジェクトをキーとして使う案が想定通りに動かなかった処理系は除いてテストしてみた。
| 処理系 | 結果 |
|---|---|
| Guile 3.0.11 | OK |
| Chez 10.4.1 | OK |
| larceny 1.3 | OK |
| IronScheme 1.0.603 | OK |
| scheme-rs 0.2.0 | NG |
| scheme-rs 最新コミット (4bfb022) | OK |
| rmosh 0.0.1 | NG |
残念ながら意図したように動かない処理系もある。
scheme-rs 0.2.0 は代入の処理に問題を抱えているが開発中のものでは直っているようだ。 rmosh 0.0.1 についてはハッシュテーブルに明瞭なバグを見つけたので報告しておいた。
実装の問題点
上述の実装をよく見た人は気づいたかもしれないが定数を返す手続きをハッシュ関数として与えている。 (これが rmosh 0.0.1 のバグを踏んだ理由でもある。)
ハッシュ関数は入力が等しければ等しいハッシュ値が生成され、等しくなくても等しいハッシュ値が生成される可能性はあるがなるべく均等に異なる値が望ましい。 ハッシュ関数が固定値を返すと速度性能は大きく劣化するかもしれないが動作はする。
どうして固定値なのかというと他に方法がないからだ。 この実装ではキーの名前自体は常に同じなので名前からハッシュ値を計算しても無意味だし、構文オブジェクトが持つ文脈情報 (contextual information) は何も操作できない。 大小関係すら定義できないので二分探索木なども使えない。 どうせ線形探索しかできないのなら劣化ハッシュテーブルでも性能は同程度だろうという割り切りでこうしている。 もちろん良くはない。
構文オブジェクトがユニークなキーとして使えるという思いつきは自分でもなかなか面白いと思ったのだが出来る操作が無さ過ぎて扱いづらかった。