この記事は、 Lisp Advent Calendar 2017 - Qiita 12日目の記事として書かれました。
最近お仕事でCommon Lispコードを書いていますが、仕様を勝手に勘違いして恥ずかしいコーディングミスをしてしまうことがどうしてもあります。 ここでは、私がやらかしたそれらをあげつらってやろうと思います。 既存のFAQとかTIPS的なものに書いてあるかとも思いますが・・ 自戒を込めて。
prog の値
ストーリー
- 関数の最初のあたりで引数を見て、だめだった場合は実行を中断したい。
- 普通の
defun
ならreturn-from
で返して終わりだけど、今回はlambda
で作る無名関数。 block
書いてlet
書いて・・とやるのは面倒くさいな- この機能が合わさった
prog
があるやん!!tagbody
の機能は使わない (go
はしない)けど、ままえやろ。
(defun make-hoge-lambda () (lambda (str) (prog ((var (parse-integer str :junk-allowed t))) ; bind して ;; チェックに失敗したら、さっさとreturn (unless (integerp var) (return "エラー発生!")) ;; そしてなんかの処理 (+ var var))))
結果
最初の確認を通った場合でも、 nil
しか返ってこない??
CL-USER> (make-hoge-lambda) #<FUNCTION (LAMBDA (STR) :IN MAKE-HOGE-LAMBDA) {100466440B}> CL-USER> (funcall * "hogehoge") "エラー発生!" CL-USER> (funcall ** "100") NIL
解説
prog
と prog*
は、 return
しないと値が返らない。 CLHS の prog, prog* の項目から引用:
results---nil if a normal return occurs, or else, if an explicit return occurs, the values that were transferred.
これはおそらく、prog
と prog*
が内包する tagbody
の機能のせいなのですが、もっとよく使う progn
では最後においた式の値が返りますから、あっさりと字面にだまされていて、 return
を忘れておりました。自縄自縛。 return
を付けて解決です。
(defun make-hoge-lambda () (lambda (str) (prog ((var (parse-integer str :junk-allowed t))) (unless (integerp var) (return "エラー発生!")) (return ; これが必要だった (+ var var)))))
あと、私はよく loop
~ finally
から戻る時に return
を忘れて、 finally
で集めた値が消えた!? と焦ります。
loop とクロージャ
ストーリー
- 少しずつ違った値を持たせた thunk を作りたいな・・
loop
でループ変数回して、それを参照するクロージャ作って返せばいいか。
(defun make-lambda-on-loop (repeat) (loop for i from 0 below repeat collect (lambda () (* i i))))
結果
全部同じ値が返るんですけど・・?
CL-USER> (make-lambda-on-loop 5) (#<CLOSURE (LAMBDA () :IN MAKE-LAMBDA-ON-LOOP) {10053780AB}> #<CLOSURE (LAMBDA () :IN MAKE-LAMBDA-ON-LOOP) {10053780DB}> #<CLOSURE (LAMBDA () :IN MAKE-LAMBDA-ON-LOOP) {100537810B}> #<CLOSURE (LAMBDA () :IN MAKE-LAMBDA-ON-LOOP) {100537813B}> #<CLOSURE (LAMBDA () :IN MAKE-LAMBDA-ON-LOOP) {100537816B}>) CL-USER> (mapcar #'funcall *) (25 25 25 25 25)
解説
loop
の for
で導入される変数は、 step されることによって更新される。そして、 step とは assign で変数の値を変えるということである。
この意味するところは、 loop
のループが回りループ変数の値が更新される際、新しい変数をつくって新しい値を bind するのではなく、 同じ変数に(破壊的に)代入する形でループ変数を更新しているということだった!!!
そのため、上のコードで作ったクロージャは全部同じ変数を参照していたのでした。実質こんな感じ:
(defun make-lambda-on-loop (repeat) (prog ((i 0) ; こう書くと、たった一つのこの変数が下の lambda の間で共有されて見える不思議 (funcs nil)) next-iteration (when (>= i repeat) (return (nreverse funcs))) (push (lambda () (* i i)) funcs) (incf i) (go next-iteration)))
今回は、 lambda
ごとに別の変数を参照させたいと思っていたので、その変数を let
して bind し直して解決です。
(defun make-lambda-on-loop (repeat) (loop for i from 0 below repeat collect (let ((i i)) ; これで新しい binding を導入。 (lambda () (* i i)))))
ちなみに、 do
と do*
は assign によってループ変数を更新 する。
dolist
と dotimes
は実装依存。
・・というのも今回調べて初めて知りました。
追記 @ 2017-12-12 夕方
やっと再発見できたので引用を追記。 http://blog.practical-scheme.net/shiro?20060110-for から:
なおCommon Lispのloopマクロやdotimesマクロはループ変数を 破壊的変更しくさるのだが、これはきっと破壊的変更の悪をプログラマに 教えんとするSteeleの親心に違いない。そうに違いないのだ。 過去に学ばないプログラマは何度も何度も何度も何度もクローズした はずの変数がいつのまにか書き換わってることに悩んで何時間も デバッグに費すのだ。おかげで昨日も締切前の貴重な時間を無駄にした。くそぅ。
handler-case と handler-bind
ストーリー
- 例外処理しないといけない・・
- 復旧処理のために restart も定義しないと。ローカル変数も掴んだ上で restart 書きたいから、
restart-case
は内側で作る感じか。 - さてハンドラは・・よう分からんけど
handler-case
で定義したらええやろ。そこでinvoke-restart
ね。
(define-condition hoge-error () ()) ;; この関数では `fuga-restart' を用意して 'hoge-error を投げる。 (defun some-function-establishes-a-restart (arg) (restart-case (error 'hoge-error) (fuga-restart () (format t "fuga-restart invoked. The arg was ~A!" arg)))) (defun some-function-establishes-a-handler () (handler-case (some-function-establishes-a-restart 'piyo) (hoge-error (e) (declare (ignore e)) ;; 上の関数の内側で立てられた `fuga-restart' を掴みたい (invoke-restart 'fuga-restart))))
結果
restart ないとか出てくるんですけど・・?
CL-USER> (some-function-establishes-a-handler) No restart FUGA-RESTART is active. [Condition of type SB-INT:SIMPLE-CONTROL-ERROR]
解説
handler-case
は、そこで作ったハンドラに制御が移る際に、動的環境を全部巻き戻してしまいます。 CLHS の handler-case の項目から引用:
In this case, the dynamic state is unwound appropriately (so that the handlers established around the expression are no longer active), and var is bound to the condition that had been signaled.
環境の巻き戻しが発生するため、 handler-case
の内側で立てた restart も、当然巻き戻しによって無くなってしまいます。
今回のように、内側で立てた restart を呼ぶには、動的環境をそのまま掴まなければならないため、 handler-bind
を使う必要があったのでした。
(defun some-function-establishes-a-handler () (handler-bind ((hoge-error (lambda (e) (declare (ignore e)) (invoke-restart 'fuga-restart)))) (some-function-establishes-a-restart 'piyo)))
CL-USER> (some-function-establishes-a-handler) fuga-restart invoked. The arg was PIYO! NIL
handler-case
と handler-bind
は並び順以外ほとんど同様に見えますが、 handler-case
では環境を巻き戻すため、上記のように内側の環境がなくなってしまうとか、 Decline が出来ないとかいう違いがあります。
ついでに restart-case と restart-bind
restart-case
と restart-bind
の違いも同様で、 restart-case
では作った restart に制御が移ると、環境を巻き戻します。
そのため、似た見た目のコードでも違いが起きます。
例えば、以下のように restart-case
を使うと、 invoke-restart
後のコードは呼ばれませんが・・:
(defun test-restart-case () (restart-case (progn (format t "~&before invoke-restart~%") (invoke-restart 'fuga-restart 5) (format t "~&after invoke-restart~%")) ; これは呼ばれない (fuga-restart (arg) (format t "~&fuga-restart invoked with ~A!~%" arg))))
CL-USER> (test-restart-case) before invoke-restart fuga-restart invoked with 5! NIL
順番が違うだけに見える restart-bind
を使うと、 invoke-restart
後のコードも呼ばれます:
(defun test-restart-bind () (restart-bind ((fuga-restart (lambda (arg) (format t "~&fuga-restart invoked with ~A!~%" arg)))) (progn (format t "~&before invoke-restart~%") (invoke-restart 'fuga-restart 5) (format t "~&after invoke-restart~%")))) ; これは呼ばれる
CL-USER> (test-restart-bind) before invoke-restart fuga-restart invoked with 5! after invoke-restart NIL
まとめ
仕様書はちゃんと読もう