【Common Lisp】
loop の中の return の違いを整理する
(DSL の句とマクロ)

  • Common Lispのloopマクロでは、returnという同じ綴りのシンボルがDSLの句とCommon Lispのマクロの2種類として存在します。
  • returnマクロは、nilという名前のblockから脱出するreturn-from nilの略記で、loopが内部でnilブロックに展開されるためdofinallyの中でも動作します。
  • 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 の引数として並んだ forwhencollectreturn などのキーワードは、 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-namenamed 句で指定しなければ nil になります。

そのため、dofinally内で、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 xwhen ... do (return x) を等価な例として並べています。

3.1. whenもマクロとDSLの句がある

ちなみに、return同様、マクロと同じ形のloop DSLの句としては、when があります。

when句ではなく、普通の Lisp の whendo節の中に書くこともできます。

;; 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だと予想しますが、実際に返り値は collectsum などの蓄積節の結果です。
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 内の returnfinally の関係も整理しておきます。

ややこしくなるのであまり混ぜない方がよいですが、returnfinallyloopにともに入れることもできます。
その場合、return句やreturnマクロが動作したときにはそのままloopは終了し finally は実行されません。
alwaysneverthereis と同様に 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 xloop DSL の句されない
when ... do (return x)普通の return マクロされない
do (when ... (return x))普通の return マクロされない
finally (return x)普通の return マクロこれ自身が finally
loop-finishloop のマクロされる

通常 loopreturn 句は、普通の 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 を使います。

  1. たとえば、C言語などの構文規則では、”if clause” を 「if節」と訳すのが一般的です。loopのforも、「for x」だけなら「句」、「for x from 1 to 10 by 2」のような全体なら「節」、というように構文範囲によっても、しっくり来る単語が違うんですよね。
  2. loopのマクロ展開は複雑で、必ずしも機械的にreturn valueが(return-from block-name value)に展開されるわけではありません。