継続とマクロでお手軽ブレークポイント

継続によるプログラムの実行の中断・再開と、LOL の pandoric macro (クロージャに捕捉されている自由変数に、クロージャの外側からアクセスするためのマクロ) を組み合わせて簡単なブレークポイントっぽいものを作ってみました。

使い方

以下のようにして使います。

(use breakpoint)

(define (fact n a)
  (bp :fact n a)
  (if (= n 0)
    a
    (fact (- n 1) (* n a))))

(bp ...) とある箇所がブレークポイントです。bp の第1引数はブレークポイントの名前、第2引数以降にはそのブレークポイントで止まったときに、その時点での値をチェックしたい変数の名前を指定します。


上のプログラムを実行したときの例を以下に示します。

gosh> (reset)  ;; プログラムを実行する前に初期化をしておく                                                  
#<undef>                                                                        
gosh> (fact 5 1)                                                    
:fact  ;; 手続き fact のブレークポイントで停止。停止したブレークポイントの名前が返ってくる                                          
gosh> (inspect n)  ;; (inspect <var>) で、ブレークした時点での変数<var>の値を確認できる                                                     
5                  ;; <var> は bp の第2引数以降で指定されている必要がある                                                
gosh> (inspect a)                                                               
1                                                                               
gosh> (resume)  ;; (resume) でプログラムの実行を再開                                                     
:fact           ;; 再び fact のブレークポイントで停止                                                               
gosh> (inspect n)                                                               
4                                                                               
gosh> (inspect a)                                                               
5                                                                               
gosh> (set!-inspect a 1) ;; (set!-inspect <var> <expr>) で、<var>の値を<expr>の値に書き換えられる                                                     
1
gosh> (inspect a)
1
gosh> (bp-enabled? #f)  ;; パラメータ bp-enabled? によってブレークポイントの有効・無効を切り換えられる                                                        
#t                                                                              
gosh> (resume)  ;; ブレークポイントを無効にして再開                                                                
24              ;; ブレークポイントで止まらず計算が最後まで行われる                                                                
gosh>


もちろん bp は普通の式なので、(when 条件 (bp ...)) のようにすることで、ある条件を満たすときだけブレークする、ということが可能です。また、ブレークしたときにはブレークポイントの名前が返ってくることから、複数のブレークポイントを設定している場合でも、以下のようにブレークポイントの名前で選別してやることができます。

(let loop ()
  (let1 x (resume)
    (cond [(eq? x ブレークポイントの名前) x]
          [(keyword? x) (loop)]
          [else x])))

さらに、ブレークした時点での情報 (inspect でチェックできる変数の値や resume を呼んだときに実行される処理) は次のブレークポイントに到達するまで更新されません。したがって、あるブレークポイントでブレークし、resume で処理を再開したらエラーが出て処理が途中で終わったという場合でも、単にもう一度 resume を呼ぶことで最後にブレークしたブレークポイントから処理を再開することができます。

コード

実装はこんな感じ。

http://gist.github.com/263565

(define-module breakpoint
  (use gauche.parameter)
  (export bp bp-enabled? inspect set!-inspect resume reset))

(select-module breakpoint)

(define %inspect #f)
(define %set!-inspect #f)

(define %cont #f)
(define %return #f)

(define bp-enabled? (make-parameter #t))

(define-syntax inspect
  (syntax-rules ()
    [(_ var)
     (%inspect 'var)]))

(define-syntax set!-inspect
  (syntax-rules ()
    [(_ var val)
     (%set!-inspect 'var val)]))
  
(define (reset)
  (define (not-suspended . _)
    (error "not suspended"))
  (call/cc
    (lambda (cc)
      (set! %inspect not-suspended)
      (set! %set!-inspect not-suspended)
      (set! %cont not-suspended)
      (set! %return cc)
      (undefined))))

(define (resume)
  (call/cc
    (lambda (cc)
      (set! %return cc)
      (%cont #f))))

(define-syntax bp
  (syntax-rules ()
    [(_ name var ...)
     (when (bp-enabled?)
       (call/cc
	 (lambda (cc)
	   (define (unknown-symbol sym)
	     (error "unknown symbol" sym))
	   (set! %cont cc)
	   (set! %inspect
		 (lambda (arg)
		   (case arg
		     [(var) var]
		     ...
		     [else (unknown-symbol arg)])))
	   (set! %set!-inspect
		 (lambda (arg val)
		   (case arg
		     [(var) (set! var val)]
		     ...
		     [else (unknown-symbol arg)])))
	   (%return name))))]))

(provide "breakpoint")

おわりに

ネタのつもりで作ったんですが、

  • ある時点での変数の値を(単に表示するだけでなく)とりだすことができる
  • ある時点での変数の値を書き換えることができる

というような点から意外と使える場面もあるのかもしれません。
ただし、内部で継続を使っているため、メインのプログラムの方で別に継続をいじる処理をしていたり、入出力とかの処理が関わるとうまく動かない場合があるかもしれません。