y2q_actionman’s ゴミクズチラ裏

内向きのメモ書きを置いてます

Common Lisp で最近やらかしたコーディングミス

この記事は、 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

解説

progprog* は、 return しないと値が返らない。 CLHS の prog, prog* の項目から引用:

results---nil if a normal return occurs, or else, if an explicit return occurs, the values that were transferred.

これはおそらく、progprog* が内包する 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)

解説

loopfor で導入される変数は、 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)))))

ちなみに、 dodo* は assign によってループ変数を更新 する。 dolistdotimes実装依存。 ・・というのも今回調べて初めて知りました。

追記 @ 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 ()
  ())
       
(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))
      (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-casehandler-bind は並び順以外ほとんど同様に見えますが、 handler-case では環境を巻き戻すため、上記のように内側の環境がなくなってしまうとか、 Decline が出来ないとかいう違いがあります。

ついでに restart-case と restart-bind

restart-caserestart-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

まとめ

仕様書はちゃんと読もう