ファイヤープロジェクト
catchとthrow
2004-01-08T20:30+09:00   matsu
Common Lispではcatchとthrowを使用して例外処理を簡潔に記述することができる.
catch関数は,第一引数に指定したタグに対して処理が投げられるとそれを受け取る.
(catch タグ 式 式 式...)
throw関数は第一引数で指定したタグを持つcatchに対して第二引数で指定した値とともに処理を投げる.「処理を投げる」というのは,gotoの構造化されたような概念である.例を示す.関数hogeがcatchの中で関数fugaを呼ぶとする.
> (defun hoge (x)   
       (format t "hoge start~%")
       (catch 'c1
         (format t "catch c1 start~%")
         (fuga x)
         (format t "catch c1 end~%"))
       (format t "hoge end~%"))
HOGE
で,関数fugaでは引数xが0だった場合にタグc1を引数にしてメッセージとともにthrowする関数だったとする.
> (defun fuga (x)
       (format t "fuga start~%")
       (if (= x 0)
           (throw 'c1 "x = 0")
         (format t "x /= 0~%"))
       (format t "fuga end~%"))
FUGA
で,hogeを呼んでみる.
> (hoge 0)
hoge start
catch c1 start
fuga start
hoge end
NIL
> (hoge 1)
hoge start
catch c1 start
fuga start
x /= 0
fuga end
catch c1 end
hoge end
NIL
hogeへの引数が0だった場合は,hogeのcatch内でfuga呼び出し後のformat(catch c1 end)が実行されていない.これはfugaのthrowでhogeのcatchまで処理が移され,hogeのcatchがそれを受けて,自身を終了させるからである.このように,catchを使用すると,関数の返り値のチェックをすることなく,かつ関数からのアクションに応じて処理を制御できる.これで返り値チェックのための繁雑なif文を省いて簡潔に例外処理を記述できる.特に関数から一気にいくつもの呼び出し元に返るときに効果を発揮する.先程をfugaを書き換えて引数が0でない場合はfooを呼び出し,fooは引数が1ならばhogeまで一気に返るように変更してみる.
> (defun fuga (x)   
       (format t "fuga start~%")
       (if (= x 0)
           (throw 'c1 "x = 0")
         (format t "x /= 0~%"))
       (foo x)
       (format t "fuga end~%"))
FUGA
> (defun foo (x)
       (format t "foo start~%")
       (if (= x 1)
           (throw 'c1 "x = 1")
         (format t "x /= 1~%"))
       (format t "foo end~%"))
FOO
hogeを呼んでみる.
> (hoge 0)
hoge start
catch c1 start
fuga start
hoge end
NIL
> (hoge 1)
hoge start
catch c1 start
fuga start
x /= 0
foo start
hoge end
NIL
> (hoge 2)
hoge start
catch c1 start
fuga start
x /= 0
foo start
x /= 1
foo end
fuga end
catch c1 end
hoge end
NIL
すべてのformatを実行するhogeに2を渡した場合と,0,1を渡した場合を比べると,処理の流れの違いがよくわかる.
catchの返り値は,throwの第二引数の値である.先のhoge,fuga,fooを元に,hogeを書き換える.
(defun hoge (x)              
  (format t "hoge start~%")
  (setf ret (catch 'c1
	      (format t "catch c1 start~%")
	      (fuga x)
	      (format t "catch c1 end~%")))
  (format t "hoge end : ret[~A]~%" ret))
HOGE
retにはcatchの返り値が格納され,hogeの最後で出力される.
> (hoge 0)                 
hoge start
catch c1 start
fuga start
hoge end : ret[x = 0]
NIL
> (hoge 1)
hoge start
catch c1 start
fuga start
x /= 0
foo start
hoge end : ret[x = 1]
NIL
> (hoge 2)
hoge start
catch c1 start
fuga start
x /= 0
foo start
x /= 1
foo end
fuga end
catch c1 end
hoge end : ret[NIL]
NIL
このようにthrowする際,第二引数にメッセージや値を指定すると,さらにthrow,catchは便利に使用できる.なお,hogeの引数に2を渡すと,fuga,fooではthrowされない.throwされない場合,catchなnilを返す.
unwind-protect内でthrowをすると,処理は該当タグのcatchに移されるが,その前にunwind-protect内の残りの式を評価する.これはthrowの有無に係わらず評価しなければならない式がある場合に便利である.前節のhogeへの変更に加えてfugaにも変更を加える.
> (defun fuga (x)
      (format t "fuga start~%")
      (if (= x 0)
          (throw 'c1 "x = 0")
        (format t "x /= 0~%"))
      (unwind-protect 
          (foo x)
        (format t "fuga end~%")))
FUGA
foo呼び出しと最後のformatをunwind-protectで囲ってみた.これで,fooでthrowされるか否かにかかわらずfugaの最後のformatは評価される.
> (hoge 0)
hoge start
catch c1 start
fuga start
hoge end : ret[x = 0]
NIL
> (hoge 1)
hoge start
catch c1 start
fuga start
x /= 0
foo start
fuga end
hoge end : ret[x = 1]
NIL
> (hoge 2)
hoge start
catch c1 start
fuga start
x /= 0
foo start
x /= 1
foo end
fuga end
catch c1 end
hoge end : ret[NIL]
NIL
hogeに1を渡したときfooでthrowされる.このとき前節まではfugaの最後のformatは評価されなかったが,今回は評価されている.なおかつfooのthrow後の処理は行なわれず,retの値が設定されていることから,fooでthrowされていることもわかる.もちろんthrowを直接unwind-protectで囲うこともできる.今度はfooに変更を加える.
(defun foo (x)    
  (format t "foo start~%")
  (if (= x 1)
      (unwind-protect 
	  (throw 'c1 "x = 1")    
	(format t "after throw~%"))
    (format t "x /= 1~%"))
  (format t "foo end~%"))
FOO
throwと追加のformat式をunwind-protectでまとめてみた.
> (hoge 0)
hoge start
catch c1 start
fuga start
hoge end : ret[x = 0]
NIL
> (hoge 1)
hoge start
catch c1 start
fuga start
x /= 0
foo start
after throw
fuga end
hoge end : ret[x = 1]
NIL
> (hoge 2)
hoge start
catch c1 start
fuga start
x /= 0
foo start
x /= 1
foo end
fuga end
catch c1 end
hoge end : ret[NIL]
NIL
hogeに1を渡したとき,fooでthrowされるが,そのthrow後のformatも実行さている.unwind-protectの返り値は,その第一引数の値である.これは,unwind-protect内でthrowされなかった場合に使用することになる.
Javaではtry,catch,finallyが同じ階層にあり,try内でthrowがある.そしてcatchされるされないにかかわらずfinallyが実行される.Lispのthrowするしないにかかわらず評価する仕組みであるunwind-protectと対象的と言えるかもしれない.
catchの引数にタグがあることからも想像できるように,catchはネストすることができる.throwはネストされたcatchのうち,どれに処理を投げるのかを第一引数のタグで指定する.例として新規にhoge,fuga,fooを作り直す.hogeはタグc1のcatch内でfugaを呼ぶ.
> (defun hoge (x)
       (format t "hoge start~%")
       (catch 'c1
         (fuga x)) 
       (format t "hoge end~%"))
HOGE
fugaではタグc2のcatch内でfooを呼ぶ.
> (defun fuga (x)
       (format t "fuga start~%")
       (catch 'c2
         (foo x))
       (format t "fuga end~%"))
FUGA
そしてfooでは引数が0ならタグc1のcatchに,引数が1ならタグc2のcatchにthrowする.
> (defun foo (x)     
       (format t "foo start~%")
       (case x
         (0 (throw 'c1 "x = 0"))
         (1 (throw 'c2 "x = 1")))
       (format t "foo end~%"))
FOO
hogeを呼んでみる.
> (hoge 0)   
hoge start
fuga start
foo start
hoge end
NIL
> (hoge 1)
hoge start
fuga start
foo start
fuga end
hoge end
NIL
> (hoge 2)
hoge start
fuga start
foo start
foo end
fuga end
hoge end
NIL
hogeの引数が0の時はfooからタグc1のcatchにthrowするので,fuga,fooの最後のformatは評価されない.hogeの引数が1の時はfooからタグc2のcatchにthrowするので,fooの最後のformatは評価されない.そしてhogeの引数が0,1以外のときはfooではthrowしないので,すべてのformatが評価される.
throwで指定したタグを引数にもつcatchが,throwの呼び出しもとにない場合,エラーとなる.
> (throw 'c1 0)

*** - THROW: there is no CATCHer for tag C1
throwをcatchするようにするのは,プログラマの責任である.
matsu(C)
Since 2002
Mail to matsu