- Common Lispの
loopマクロでは、returnという同じ綴りのシンボルがDSLの句とCommon Lispのマクロの2種類として存在します。 returnマクロは、nilという名前のblockから脱出するreturn-from nilの略記で、loopが内部でnilブロックに展開されるためdoやfinallyの中でも動作します。return句は、do (return-from block-name value)に等価で、do節の中でloopブロックから抜ける処理です。finally節はループ後の後処理を実行する場所であり、それ自体は返り値を作らないため、値を返すにはreturnマクロを明示的に書く必要があります。

文法では、一般的に”clause”は「節」と訳し、”phrase(句)”と区別されますが、しばしば Common Lisp の構文の説明では、「〜句(clause)」と表記されることが多いです。
ここでは、構文要素は慣例に従って「句」と書きつつ、複文の一部を成す clause (doやfinallyなど)は「節」と書いたりして、表記が揺れているのですが、厳密さが気になる場合は、適宜読み替えて理解してください1。
1. return句とreturnマクロ
loop を書いていると、同じ return という語が文脈によって違うことに気づきます。
;; A: loop の句として return
(defun find-first-even-a (lst)
(loop for x in lst
when (evenp x)
return x))
;; B: do の中で return マクロ
(defun find-first-even-b (lst)
(loop for x in lst
when (evenp x)
do (return x)))
;; C: finally の中で return マクロ
(defun freq-vector (lst size)
(loop with result = (make-array size :initial-element 0)
for x in lst
do (incf (aref result x))
finally (return result)))
(find-first-even-a '(1 3 4 7)) ;=> 4
(find-first-even-b '(1 3 4 7)) ;=> 4
(freq-vector '(0 1 2 1 0 2 2) 3) ;=> #(2 2 3)Code language: Lisp (lisp)
A と B は同じ動作をしますが、B の return は、構文上はCと同じ位置にあります。
1.1. 前提:loop の中には「読まれ方の層」がある
loop は、複雑な内部構造を持つマクロです。
コンパイル時に展開され、prologue(前処理)・body(反復本体)・epilogue(後処理)の三部構成になります。
loop の引数として並んだ for、when、collect、return などのキーワードは、 loop マクロが文法要素として読みます。
これを「loop DSL の句(clause)」と呼びます。
一方、do (...) や finally (...) などの括弧の中に書いたコードは、通常の式として評価されます。
つまり、同じ return という綴りのシンボルでも、どちらにあるかで意味が変わります。
2. return-from 特殊形式 と block
Common Lispの returnマクロは、return-fromという特殊形式(special operator)で使っています。
return-from の機能は、対応する block から即座に脱出し、result の値を返すことです。
(defun find-even (lst)
(block search
(dolist (x lst)
(when (evenp x)
(return-from search x)))))
(find-even '(1 3 4 7))
;=> 4Code language: Lisp (lisp)
ここでは、シンボル searchは評価されず、result だけが評価され、blockの式の値となります。
2.1. return マクロ
return マクロは、nil という名前のブロックから抜けます。
つまり、(return x) は、(return-from nil x) の略記で、nil という名前のブロックが存在する文脈でのみ機能します。
言語仕様(HyperSpec)の定義では、loop マクロは内部でblockとして展開されますが、block-name は named 句で指定しなければ nil になります。
そのため、doやfinally内で、returnマクロでloopから抜けることができるのです。
;; B.
(defun find-first-even-b (lst)
(loop for x in lst
when (evenp x)
do (return x)))
(find-first-even-b '(1 3 4 7 6))
;=> 4Code language: Lisp (lisp)
2.2. ブロック名のあるloop
ちなみに、loopに、明示的にたとえば、outerというブロック名を付けた場合には、returnマクロではなく return-fromマクロを使います。
(defun search-matrix (matrix target)
(loop named outer
for row in matrix
do (loop for x in row
when (= x target)
do (return-from outer x))))
(search-matrix '((1 2 3) (4 5 6) (7 8 9)) 5)
;=> 5
(search-matrix '((1 2 3) (4 5 6) (7 8 9)) 15)
;=> NILCode language: Lisp (lisp)
loopのブロック名は多重ループから抜けるのに使われます。return マクロは、nil という名前の block から抜けるため、別の nil ブロックがあればそちらに作用します。
つまり、外側の loop named outer を直接には抜けられないのです。
3. loop の return 句とは何か
一方、loop の引数として書く return シンボルは、loop DSLの句です。
return value 句は、Common Lisp の return マクロとは別物で、言語仕様では概念上 do (return-from block-name value) と等価とされています2。
つまり、自動的にloopのブロック名も補われます。
ただ、多くの loop は名前なしなので、return 句は概念上 (return-from nil value) であり、(return value)と同じです。
;; A.
(defun find-first-even-a (lst)
(loop for x in lst
when (evenp x)
return x))
(find-first-even-a '(1 3 4 7 6))
;=> 4Code language: Lisp (lisp)
この書き方が、 loop DSL として最も自然な書き方です。
言語仕様では when ... return x と when ... do (return x) を等価な例として並べています。
3.1. whenもマクロとDSLの句がある
ちなみに、return同様、マクロと同じ形のloop DSLの句としては、when があります。
when句ではなく、普通の Lisp の when を do節の中に書くこともできます。
;; B2. do の中はすべて普通の Lisp 式
(defun find-first-even-b2 (lst)
(loop for x in lst
do (when (evenp x)
(return x))))
(find-first-even-b2 '(1 3 4 7 6))
;=> 4Code language: Lisp (lisp)
loop の外殻だけを使い、中身は通常の Lisp 式で構成しています。
loop DSLの句を知らなくても、このような Lisp 式で代用できるわけですが、あまり自然な書き方ではありません。
というのも、loopマクロは、「読みやすい繰り返し構文」を作る設計になっています。
そのため、S式としてはトリッキーですが、括弧の入れ子は浅くできるのです。find-first-even-b2の括弧終端は4重ですが、find-first-even-aは2重括弧に収まっています。
4. finally は返り値を指定する構文ではない
returnマクロとともに使われる構文に、finally句があります。
finallyは、終端なので放っておいてもブロックから脱出します。
なぜ、returnマクロを使うのでしょう。
(loop with result = 1
for x in '(1 2 3)
do (setf result (* result x))
finally (return result))
;;=> 6
;; returnがないとエラーになる
(loop with result = 1
for x in '(1 2 3)
do (setf result (* result x))
finally result)
;;=> SIMPLE-PROGRAM-ERROR "~? current LOOP context:~{ ~S~}."Code language: Lisp (lisp)
finally節に、値を直接入れると構文エラーになります。finally は任意式を値として返す位置ではなく、終端処理の節です。finallyの節にはループの通常終了後に一度だけ実行する「処理」を入れ、そのままでは返り値には関与しません。
(defun collect-and-print (lst)
(loop for x in lst
collect x
finally (format t "done~%")))
(collect-and-print '(1 2 3))
; done
;=> (1 2 3)Code language: Lisp (lisp)
一見、loopの返り値は、最後に評価された (format t "done~%")の値 NILだと予想しますが、実際に返り値は collectやsum などの蓄積節の結果です。finally の基本は、後処理を走らせることだけなのです。
4.1. finally – return で返り値を上書き
最終結果を明示したいときには、finallyの処理としてreturnマクロを書きます。
;; C: finally の中で return マクロ(頻度表をベクタで返す)
(defun freq-vector (lst size)
(loop with result = (make-array size :initial-element 0)
for x in lst
do (incf (aref result x))
finally (return result)))Code language: Lisp (lisp)
finally の中に書いた return マクロは、nil ブロックから明示的に脱出するだけでなく、暗黙の返り値を上書きします。
つまり、returnマクロを使うのは返り値を上書きするためです。
複数の蓄積節の値をまとめて返す場合には、finally ... returnは使われます。
;; into で蓄えた値を finally で組み合わせて返す
(defun stats (lst)
(loop for x in lst
sum x into total
count x into n
finally (return (list :sum total :count n :avg (/ total n)))))
(stats '(1 2 3 4 5))
;=> (:SUM 15 :COUNT 5 :AVG 3)Code language: Lisp (lisp)
into を使うと蓄積節の暗黙の返り値が作られないため、finally の中で return を書かないと nil が返ります。
4.2. return は finally を飛ばす
ちなみに、loop 内の return と finally の関係も整理しておきます。
ややこしくなるのであまり混ぜない方がよいですが、returnとfinallyをloopにともに入れることもできます。
その場合、return句やreturnマクロが動作したときにはそのままloopは終了し finally は実行されません。always、never、thereis と同様に finally を飛ばします。
(defun find-even-with-finally (lst)
(loop for x in lst
when (evenp x)
return x
finally (format t "finally ran~%")))
(find-even-with-finally '(1 3 4 7))
;=> 4
; finally は実行されないCode language: Lisp (lisp)
4.3. loop-finishマクロはfinallyを通る
その代わり、ループを終了するときに通常終了としてfinallyを実行するために、loop-finish マクロが用意されています。
ループ本体の途中から finally を通って抜けたい場合に使います。
(defun find-even-via-finish (lst)
(loop for x in lst
for result = nil
do (when (evenp x)
(setf result x)
(loop-finish))
finally (return result)))
(find-even-via-finish '(1 3 4 7))
;=> 4Code language: Lisp (lisp)
つまり、return は結果を返して即脱出、loop-finish は通常終了へ送る、という使い分けです。
5. まとめ
早期脱出を一行で書くなら when ... return x。
loop DSL の流儀に最も素直で、読み手に意図が伝わりやすいです。
| 書き方 | return の正体 | finally の実行 |
|---|---|---|
when ... return x | loop DSL の句 | されない |
when ... do (return x) | 普通の return マクロ | されない |
do (when ... (return x)) | 普通の return マクロ | されない |
finally (return x) | 普通の return マクロ | これ自身が finally |
loop-finish | loop のマクロ | される |
通常 loop の return 句は、普通の return マクロと同じ block への脱出に落ちます。
どちらの層にあるかが違うだけで、名前のない loop なら結果は一致します。
ただ、副作用のある処理と脱出を同時に書くなら do (progn ... (return x)) の形が自然です。
(defun find-and-log (lst)
(loop for x in lst
when (evenp x)
do (progn
(format t "found: ~a~%" x)
(return x))))
(find-and-log '(1 3 4 7))
; found: 4
;=> 4Code language: Lisp (lisp)
finally は返り値を作る節ではありません。
その中で return を書いたときだけ、明示的な返り値になります。
into で複数の値を蓄積して最後に組み立てるなら finally (return ...) を使います。
finally の中の処理を必ず実行してから抜けたい場合は、return ではなく loop-finish を使います。