PLT SchemeのREPLで直前の値を参照できるようにする

CLの処理系にも似たようなのがあると思いますが、HaskellのGHCiで、直前に評価された値にitでアクセスできるというのがあったので、真似してみました。

#lang scheme/base

(provide it)

(define it~ '())
(define eval~ (current-eval))

(define-syntax it
  (syntax-id-rules (set!)
    ((set! it _) (error 'it "is not modifiable."))
    ((it e ...) ((car it~) e ...))
    (it (apply values it~))))

(current-eval
 (lambda (x)
   (call-with-values (lambda () (eval~ x))
     (lambda results
       (unless (or (null? results)
                   ;; retain the old value if the new one was void
                   (and (null? (cdr results))
                        (void? (car results))))
         (set! it~ results))
       (apply values results)))))

デフォルトのevalをオーバーライドし、evalの結果 (results) を保存する処理を挟んでいます。resultsは多値をリスト化したものなので、それを改めて多値に変換するマクロとしてitを定義してあります。

itの定義にはsyntax-id-rulesを使っています。変数のように扱えるマクロを作るマクロです。変数的な振る舞いは3番目の節で定義しています。

関数適用の形でも使えるようにするには2番目の定義が必要です。順番が重要です。これを後に持ってきてしまうと関数適用の形式が捕捉できなくなるので注意しましょう。

あと細かいですが、GHCiではlet構文で変数束縛を行った後でも古いitが残るみたいなので、それも真似てみました (PLTではdefineの結果voidが返る、という挙動に基づいています)。


実行例

> (+ 1 2)
3
> it
3
> (+ it 4)
7
> list
#<procedure:list>
> (it 1 2 3)
(1 2 3)
> (thread
   (lambda ()
     ((lambda (f) (f f))
      (lambda (f)
        (display ".") (f f)))))
#<thread>
> .....................................(kill-thread it)
..........>

スレッドなど、readできない形式の値をグローバル変数にバインドし忘れた場合でもアクセスが可能になるので便利ですね。

なお、こういったREPLの拡張機能は次のようにコマンドライン引数で自動的に読み込まれるようにすると良いです。

(setq scheme-program-name "mzscheme -t /path/to/init/file -i")