【Common Lisp】
loop の基本の使い方と構文定義

  • Common Lisp の loop は、キーワードを組み合わせて柔軟にループを記述する構文です。
  • forrepeat の回数指定や、collectsumcount での集計 while/until による条件終了など、多様な節があり、組み合わせることで幅広い処理を表現できます。

ちょっと Lisp らしくない英語風のキーワードが並ぶ loop の文法を俯瞰していきます。

関連記事

1. 【基本】まず loop を回す(do)

loop は Common Lisp の中でひときわ異彩を放つ「構文」です。

まずは、0〜9までの数字を出力してみます。

(defun print-numbers ()
  (loop for i from 0 to 9
        do (format t "~a" i)))

(print-numbers)
;=> 0123456789Code language: Lisp (lisp)

for i from 1 to 9 で、カウンタ変数 i を 1 から 9 まで動かし、doの式を実行します。
doは、結果を返さない場合に使い、NILになります。

C言語で言えば、以下のような for の制御構文と同じことをしています。

#include <stdio.h>

void print_numbers() {
  for (int i = 0; i <= 9; i++) {
    printf("%d", i);    
  }
}Code language: C++ (cpp)

1.1. forでカウンタ変数を指定する

forで指定するカウンタ変数は、好きな変数名にできます。

たとえば、a 〜 z までの文字コードを順番に文字として(code-char)出力しています。

(defun print-alphabets ()
  (loop for ch from (char-code #\a) to (char-code #\z)
    do (format t "~a" (code-char ch))) )

(print-alphabets)    
;=> abcdefghijklmnopqrstuvwxyz    Code language: Lisp (lisp)

1.2. リストを作る(collect)

ただ、Lisp では loop の結果を別の式で使うことの方が多いです。

式がすべて値を持つように、loop も通常は値を返します1

たとえば、collect はリストを返します。
1から n までの2乗リストを作る例です。

(defun squares (n)
  (loop for i from 1 to n
        collect (* i i)))

(squares 5)
;=> (1 4 9 16 25)Code language: Lisp (lisp)

ちなみに、先ほどのprint-numbers関数の do をそのまま collect に変えると、formatの戻り値 NIL を集めることになります。

(defun print-numbers-collect ()
  (loop for i from 0 to 9
        collect (format t "~a" i)))

(print-numbers-collect)
;=> 0123456789
;=> (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)Code language: PHP (php)

1.3. sumで合計を返す

collect がリストを返すのに対して、
sum は、数値の合計を返します。

(defun square-total (n)
  (loop for i from 1 to n
        sum (* i i)))

(square-total 5)
;=> 55Code language: Lisp (lisp)

sumは、蓄積変数を作り、(* i i) で各ステップの値を加算し、値として返します。
listsumの2つが loop の基本的な出力の形です。

1.4. リストの繰り返し in

インデックスではなく、リストの要素を直接取り出すには in を使います。
文字列リストの各要素を文字数に変換する例です。

(defun word-lengths (words)
  (loop for word in words
        collect (length word)))

(word-lengths '("cat" "elephant" "dog" "butterfly"))
;=> (3 8 3 9)Code language: Lisp (lisp)

for word in リスト はリストの要素を順に word に束縛します。
インデックスを気にせず書けるので、コレクションを処理するときは fromto より自然な選択肢です2

1.5. 条件に合うものだけ(when / unless)

ループ中で条件に応じて処理を分けるには、whenunless を使います。

when / unless の後には、collectsumdo などの節を続けることができます。
条件に応じたフィルタ処理を簡潔に表現できます。

  • when は条件が真(true)のときに実行し、
  • unless は条件が偽(false)のときに実行します。
; 偶数だけを集める
(defun collect-even (n)
  (loop for i from 1 to n
        when (evenp i)
          collect i))

(collect-even 10)
;=> (2 4 6 8 10)Code language: Lisp (lisp)

つまり、「条件に合うときだけ集める」「条件に合うときだけ副作用を実行する」といった書き方が可能です。

1.6. finally, return で値を返す

loopで好きな値を返したいときには、finallyとreturnを組み合わせます。

通常のループ終了後に一度だけ処理を走らせるには finally を使います。
この中で return を使うと、たとえば副作用だけの do のループでも値を返せます。

(defun process-items (items)
  (loop for item in items
        do (format t "処理中: ~a~%" item)
        finally (return (length items))))

(process-items '("alice" "bob" "carol"))
; 処理中: alice
; 処理中: bob
; 処理中: carol
;=> 3Code language: Lisp (lisp)

反復が終了したら、finallyが一度だけ実行され、値を返します3

1.7. withで蓄積変数を作る

for で作った変数はループのたびに更新されます。
そこで、ループ内で自分で更新したい変数は、with で初期化します。

finallyと組み合わせて、 n の階乗を求める例です。

(defun factorial (n)
  (loop with product = 1
        for i from 1 to n
        do (setf product (* product i))
        finally (return product)))

(factorial 10)
;=> 3628800Code language: Lisp (lisp)

withで蓄積変数 productを導入し、最後に結果を返しています。

2. 【応用】繰り返しの形を広げる

loopは、さまざまな節(clause)を組み合わせて、いろんなループ構造を表現できます。

ここまでの節の役割は、大きく分けると

  • 【変数節】カウント変数の定義を範囲を指定する for, from-to, in
  • 【蓄積節】蓄積結果を指定する collect, sum
  • 【条件節】条件に合うときに処理をする when, unless
  • 【変数節と終了節】変数を作って最後に結果を返す with, finally-return

これらを組み合わせると、いろんな反復構造を作れました。
しかし、loopには、ほかにもいろんなキーワードが用意されています。

2.1. countで条件を数える

collectは、要素をリストにまとめ、sumは、数値を合計します。
countは、条件式を評価して、真になった要素数を数えます。

; 1から n の中で3の倍数の個数
(defun count-multiples-of-3 (n)
  (loop for i from 1 to n
        count (zerop (mod i 3))))

(count-multiples-of-3 30)  ;=> 10Code language: Lisp (lisp)

2.2. 回数で繰り返す(repeat)

回数だけ指定して繰り返すには repeat を使います。

モンテカルロ法で円周率を近似する例です。

(defun estimate-pi (trials)
  (let ((hits (loop repeat trials
                    count (let ((x (random 1.0))
                               (y (random 1.0)))
                            (<= (+ (* x x) (* y y)) 1.0)))))
    (* 4.0 (/ hits trials))))

(estimate-pi 1000000)
;=> 3.14...  ; 実行ごとに近似値が変わるCode language: Lisp (lisp)

repeat はカウンタ変数を作りません。
「何回やるか」だけが重要なときに使います4

2.3. 複数のループ変数を指定する(for)

forやrepeatは、同時に置けます。
その場合は、いずれかの終了条件が訪れたところで終了します。

(defun take (n lst)
  (loop repeat n
        for x in lst
        collect x))

(take 3 '(10 20 30 40 50))
;=> (10 20 30)

(take 10 '(10 20 30))
;=> (10 20 30)Code language: Lisp (lisp)

また、複数のループ変数を置くこともできます。

(defun zip-with-index (lst)
  (loop for i from 1
        for x in lst
        collect (list i x)))

(zip-with-index '("alice" "bob" "carol"))
;=> ((1 "alice") (2 "bob") (3 "carol"))Code language: Lisp (lisp)

i には終端がないので無限に続きますが、x がリストの末尾に達した時点でループ全体が終了します。

2.4. 刻み幅のby

刻み幅を変えるには by を使います。
初項と公差を指定して、等差数列を生成する例です。

(defun arithmetic-sequence (start step num)
  (loop for i from start by step
        repeat num
        collect i))

(arithmetic-sequence 3 4 5)
;=> (3 7 11 15 19)Code language: Lisp (lisp)

2.5. below句とベクタ

to は終端を含みますが、含まない場合は below を使います。

たとえば、ベクタの全要素を aref で取り出す場合、(length v) を終端に指定すると自然に書けます5

また、forのループ変数の初期値、from は省略でき、その場合は 0 始まりになります。

(defun vector->list (v)
  (loop for i below (length v)
        collect (aref v i)))

(vector->list #(10 20 30 40 50))
;=> (10 20 30 40 50)Code language: Lisp (lisp)

ちなみに、forには別名asがありますが、一般的にはforが使われます。
また、fromtoにも、 upfrom, uptoという別名があります。
というのも、downfrom, downtoという1ずつ下がるループもあるからです。

loopのキーワードには、同じ意味や似た意味の言い換えがたくさんあります。

2.6. on句 リストの走査

onは、リストの残り(cdr)を取りながらループします。

2.7. across 配列の繰り返し

acrossは、ベクタや文字列などの配列形式で、一つずつループします。

2.8. 継続条件と終了条件(while, until)

終端ではなく条件式で繰り返しを止めることもできます。
それが、whileとuntilです。

while は条件が真である間ループを続けます。

; while:続ける条件
(defun collatz-while (n)
  (loop while (> n 1)
        collect n
        do (setf n (if (evenp n) (/ n 2) (+ (* 3 n) 1)))))

(collatz-while 6)  ;=> (6 3 10 5 16 8 4 2)Code language: Lisp (lisp)

until は条件が真になったらループを終了します。

; until:やめる条件
(defun collatz-until (n)
  (loop until (= n 1)
        collect n
        do (setf n (if (evenp n) (/ n 2) (+ (* 3 n) 1)))))

(collatz-until 6)  ;=> (6 3 10 5 16 8 4 2)Code language: Lisp (lisp)

この例では同じ結果になりますが、while は「続ける条件」、until は「やめる条件」と読み方が逆です。

2.9. 条件に合う要素を1つ探す(thereis)

thereis は、リスト検索に使われます。
条件が成立した要素が見つかった時点でその値を返し、その後の繰り返しは実行しません。

; リストの中に平方数が含まれるか調べ、最初に見つかったものを返す
(defun find-perfect-square (lst)
  (loop for n in lst
        thereis (when (= n (expt (isqrt n) 2)) n)))

(find-perfect-square '(6 10 15 16 21))  ;=> 16Code language: Lisp (lisp)

2.10. 全条件の判定(always, never)

リストなどで全て条件を満たすか調べる条件・蓄積節が、alwaysとneverです。

always はすべての要素で条件が成立すれば T を返し、失敗した時点で即座に NIL を返して終了します。
never はその逆で、一度も条件が成立しなければ T を返します。

; リストがソート済みか検証する
(defun sorted-p (lst)
  (loop for (a b) on lst
        while b
        always (<= a b)))

(sorted-p '(1 3 5 7 9))  ;=> T
(sorted-p '(1 3 2 7 9))  ;=> NILCode language: Lisp (lisp)

whileuntil との大きな違いは、これらが値を返しながら早期終了する点です。
whileuntil は繰り返しの継続を制御するだけで、値は返しません6

3. 【ルール】不思議なマクロ loopの構文

loop は、関数ではなく「マクロ」です。
マクロとはソースコードを受け取って別の Lisp コードに変換する仕組みで、loop コンパイル時に展開されます。

任意の loop 式を見て「これは何をしているのか」を判断するには、節の種類と組み合わせのルールを押さえる必要があります。

実は、loop にはもう1つの形があります。
ここまで、扱った loop は、節を持つ「拡張 loop」で、
節を持たない形を simple loop と呼び、無限ループになります。

REPL のような入力待ちループがその典型です。

(defun repl ()
  (loop
    (format t "> ")
    (let ((input (read-line)))
      (when (string= input "quit")
        (return :bye))
      (format t "~a~%" (eval (read-from-string input))))))Code language: Lisp (lisp)

単純 loop は、逐次実行 progn や 変数定義 letのように、S式を並べるだけのシンプルな特殊形式です。

単純 loop か 拡張 loopかは、loop直後 に節キーワードが来るかどうかで区別されます7
forcollectsum といったキーワードは、通常の Common Lisp キーワードシンボルである :foo の形のものとは異なり、loop マクロが名前で認識する専用の語です8

loop が Lisp らしくない理由は、そもそも Lisp の外のプログラミング文化を取り込んだ構文だからです。

3.1. loopの構文規則(簡易BNF)

HyperSpec には loop の 構文の定義が載っています9

一部を簡略化して形で示すと、loopは 主に変数節とメイン節に分かれることがわかります。

loop ::= (loop [named 名前]
               {変数節}*
               {メイン節}*)

変数節 ::= with節 | for-as節 | 初期・終了節

メイン節 ::= 無条件実行節(do, return)
         | 値蓄積節(リスト蓄積, 算術蓄積)
         | 終了条件節(while, until, repeat, always, never, thereis)
         | 条件節(if, when, unless, else, end)
         | 初期・終了節(initially, finallyfor-as節 ::= for var [of-type type]
               (from-to-spec | in-spec | ...)Code language: JavaScript (javascript)

{変数節}* は変数節を何個でも並べられることを意味します。
{メイン節}* も同様です。
[named name] は省略可能で、省略すると暗黙のブロック名 nil が使われます。

種類備考
変数節foras)、withforas は同義語
無条件実行節dodoing)、return
値蓄積節collectappendnconc
sumcountmaximizeminimize
各節に -ing 形の別名あり(collecting など)
終了条件節repeatwhileuntil
alwaysneverthereis
for/as の範囲指定も終了条件を持つ
条件節ifwhenunlesselseendelse/end は条件節の構成要素
初期化・終了節initiallyfinally変数節にもメイン節にも書ける

3.2. マクロ展開と実行順序

拡張 loop のマクロ展開後の形は

  • prologue(初期化処理)、
  • body(各イテレーション)、
  • epilogue(終了処理)

の3部で構成されます。

initially は必ずループ開始前に、finally はループの通常終了後に実行されます。

それ以外の節は書いた順に実行されます。
ただし、for 節の初期化は with より前に行われます。

4. 反復の前後に関する節

4.1. namedreturn-from で二重ループから出る

loop は暗黙に nil という名前のブロックを作ります。
named でその名前を変えられます。
二次元グリッドから条件を満たす座標を探す例です。

(defun find-negative (matrix)
  (loop named search
        for row from 0 below (array-dimension matrix 0)
        do (loop for col from 0 below (array-dimension matrix 1)
                 when (minusp (aref matrix row col))
                   do (return-from search (list row col)))))

(find-negative #((1 2 3)(4 -5 6)(7 8 9)))
;=> (1 1)Code language: Lisp (lisp)

4.2. initiallyは初期化処理

finally がループ終了後に走るのに対し、initially はループ開始前に一度だけ走ります。10

(defun count-primes-verbose (limit)
  (loop initially (format t "~a 以下の素数を探索~%" limit)
        for n from 2 to limit
        when (loop for d from 2 to (isqrt n)
                   never (zerop (mod n d)))
          count t into total
        finally (format t "個数: ~a~%" total)))

(count-primes-verbose 20)
; 20 以下の素数を探索
; 個数: 8Code language: Lisp (lisp)

5. 変数節は複数並べられる

for 節は複数並べてかまいません。

(defun zip-with-index (lst)
  (loop for i from 1
        for x in lst
        collect (list i x)))

(zip-with-index '("alice" "bob" "carol"))
;=> ((1 "alice") (2 "bob") (3 "carol"))Code language: Lisp (lisp)

インデックスとリスト要素を同時に取り出す典型的な使い方です。
複数の for 節がある場合は、いずれかが終了条件に達した時点でループ全体が終了します11

5.1. リストの分配束縛

for の変数部分にリスト構造を書くと、要素を分配して受け取れます。

2次元ベクトルの内積リストを計算する例です12

(defun dot-products (pairs-a pairs-b)
  (loop for (ax ay) in pairs-a
        for (bx by) in pairs-b
        collect (+ (* ax bx) (* ay by))))

(dot-products '((1 0) (0 1) (3 4))
              '((1 0) (1 0) (4 3)))
;=> (1 0 24)Code language: Lisp (lisp)

5.2. of-typeはコンパイラへの型宣言

コンパイラへのヒントとして型を宣言できます。

大きな数値の累積演算など、パフォーマンスが気になる場面で使います。

(defun fast-sum (n)
  (loop for i of-type fixnum from 1 to n
        sum i))

(fast-sum 1000000)
;=> 500000500000Code language: Lisp (lisp)

6. そのほかの蓄積節(append, nconc, maximize, minimize )

値を蓄積する節は Value Accumulation Clauses と呼ばれます。
1章で見た collectsumcount のほかにもいくつかあります。

動作
collect値をリストに集める
appendリストを連結する
nconcリストを破壊的に連結する
sum数値を加算する
count真になった回数を数える
maximize最大値を返す
minimize最小値を返す

数列を題材に各節を確認します。

; 複数の素因数リストをフラットに展開する
(defun flatten-factor-lists (factor-lists)
  (loop for factors in factor-lists
        append factors))

(flatten-factor-lists '((2 3) (5 7) (11 13)))
;=> (2 3 5 7 11 13)Code language: Lisp (lisp)

maximizeは、最大値だけでなく、「最後の値」という意味でも使われます。

; フィボナッチ数列の第 n 項
(defun fib-max (n)
  (loop for a = 0 then b
        for b = 1 then (+ a b)
        repeat n
        maximize b))

(fib-max 10)  ;=> 55Code language: Lisp (lisp)

nconcappend と似ていますが、引数のリストを破壊的に変更して連結します。
新しいコンスセルを作らない分だけメモリ効率がよい場合がありますが、元のリストが変更されるため、リテラルリスト('(1 2 3) のような式)に対して使うと動作が未定義になります。13

6.1. 複数の蓄積節と into

通常、蓄積節は一種類しか使えません。
たとえば、collectsum をそのまま並べると、どちらの値を返すか定義されていません。

そこで、sumcountなどで複数の蓄積結果が必要なときは必ず into で変数に入れます。
into を使うと結果を変数に蓄えつつ、finally で好きな形にして返せます14

たとえば、平均を求めるには

(defun average (numbers)
  (loop for x in numbers
        sum x into total
        count x into n
        finally (return (/ total n))))

(average '(1 2 3 4 5))
;=> 3Code language: Lisp (lisp)

逆に言うと、into を使わない場合、loop は蓄積節の結果を暗黙に返します。

7. 条件をつける(when, unless, if)

ループ本体の中で条件分岐するには whenunlessif を使います。
文字列から数字だけを抽出する例です。

; 数字文字だけ取り出す
(defun extract-digits (str)
  (loop for c across str
        when (digit-char-p c)
          collect c))

(extract-digits "a1b2c3")      ;=> (#\1 #\2 #\3)Code language: Lisp (lisp)
; 数字以外を取り出す
(defun extract-non-digits (str)
  (loop for c across str
        unless (digit-char-p c)
          collect c))

(extract-non-digits "a1b2c3")  ;=> (#\a #\b #\c)Code language: Lisp (lisp)

when が真のときに後続の節を実行し、unless は偽のときに実行します。

7.1. 条件式の値は it で参照できる

it は直前の条件式の値を参照する擬似変数です。
条件の評価結果を再利用するときに使います。
assoc でシンボルテーブルを引く例が典型的です。

(defun lookup-entries (keys table)
  (loop for key in keys
        when (assoc key table)
          collect it))

(lookup-entries '(:x :w :y :v :z)
                '((:x 42) (:y 7) (:z 99)))
;=> ((:X 42) (:Y 7) (:Z 99))Code language: Lisp (lisp)

it は通常の変数 IT と名前が衝突する可能性があります。IT という名前の変数をループ内で使うときは注意が必要です。15

7.2. if, elseで振り分ける

ifelse と組み合わせて、偶数と奇数の和を同時に求める例です。

(defun sum-even-odd (n)
  (loop for i from 1 to n
        if (evenp i)
          sum i into even-sum
          else sum i into odd-sum
        finally (return (list :even even-sum :odd odd-sum))))

(sum-even-odd 10)
;=> (:EVEN 30 :ODD 25)Code language: Lisp (lisp)

7.3. andで条件を並べる

条件節の中に複数の節を並べるには and を使います。
素数を集めつつ個数も取る例です。

(defun primes-with-count (limit)
  (loop for n from 2 to limit
        when (loop for d from 2 to (isqrt n)
                   never (zerop (mod n d)))
          collect n
          and count t into total
        finally (return (values * total))))

(primes-with-count 20)
;=> (2 3 5 7 11 13 17 19)
;   8Code language: Lisp (lisp)

valuesは、collect n で作ったリストを受け取り、二値でreturnしています。

7.4. 条件節の中に蓄積節を入れられる、明示的なend

whenunlessif の後には蓄積節や無条件実行節を続けられます。
ネストは end で閉じます。

条件節が深くネストする場合は end で明示的に閉じることができます。

FizzBuzz のうち両方の条件を満たす数(15の倍数)だけを集める例です。

(defun fizzbuzz-both (limit)
  (loop for n from 1 to limit
        when (zerop (mod n 3))
          when (zerop (mod n 5))
            collect n
          end
        end))

(fizzbuzz-both 30)
;=> (15 30)Code language: Lisp (lisp)

でも、whenの条件式で and関数を使ってもよさそうです。

  1. loop マクロは展開後、内部的に blocktagbody を含む形になります。戻り値はその block から返されます。 – CLHS: Section 6.1.1.4 Expanding Loop Forms
  2. foras は完全な同義語で、どちらを使っても動作は同じです。文体的な好みで使い分けられます。 – CLHS: Section 6.1.2.1
  3. ただし、finally は、常に実行されるとは限らない点には注意が必要ですalwaysneverthereis による早期終了では実行されません。ループ本体の深い場所から通常終了と同じ手順で抜けるには、loop-finish というローカルマクロが使えます。loop-finishfinally 節を実行してから終了します。 – CLHS: Local Macro LOOP-FINISH
  4. forasrepeat といった変数節は、他の節(docollect など)より前に書かなければなりません。ただし initiallywithnamed は例外で、変数節と混在できます。 – CLHS: Section 6.1.2.1
  5. 数値範囲の指定には to(終端を含む)、below(終端を含まない)、above(逆方向で終端を含まない)、downto(逆方向で終端を含む)があります。 – CLHS: Section 6.1.2.1 Iteration Control
  6. alwaysneverthereis は内部的に return を使って終了するため、finally 節が実行されません。一方、whileuntil はループの後始末(epilogue)に制御を渡すため、finally 節が実行されます。 – CLtL2: Section 26.7 End-Test Control
  7. simple loop と extended loop の違いは HyperSpec の Macro LOOP のページで説明されています。loop-finish は simple loop 内では使えません。 – CLHS: Macro LOOP
  8. loop マクロが展開されると、変数の束縛に let または lambda に相当する形が使われます。初期値の計算は書いた順に行われますが、束縛のタイミングは実装依存の場合があります。 – CLHS: Section 6.1.1.4
  9. BNF の完全な定義は HyperSpec の Macro LOOP のページで参照できます。仕様の疑問が生じたときの最終的な参照先は HyperSpec の第 6.1 節です。 – CLHS: Macro LOOP
  10. initially の処理は prologue(前処理)として展開され、変数の初期化処理よりも後、最初のイテレーションよりも前に実行されます。 – CLHS: Section 6.1.1.4
  11. 複数の for 節を and でつなぐと、変数の更新が並列に行われます。and なしで並べると逐次的に更新されます。 – CLHS: Section 6.1.2.1
  12. 分配束縛はネストしたリスト構造にも対応しています。たとえば for ((a b) c) in ... のように書くと、各要素のサブリストの先頭2要素と残りを別々に受け取れます。 – CLHS: Section 6.1.1.7 Destructuring
  13. nconc は最後の引数以外のリストの末尾 CDR を書き換えます。同じリストを2度 nconc に渡すと循環リストになります。 – CLHS: Function NCONC
  14. 各蓄積節には -ing 形の別名があり、collect/collectingsum/summingcount/counting のように使えます。動作の違いはなく、文体的な好みで選べます。 – CLtL2: Section 26.8 Value Accumulation
  15. itloop の条件節に固有の擬似変数で、when/unless/if のテスト式の評価結果を保持します。ループ内で IT という名前の通常変数を使うと、この擬似変数と衝突する可能性があります。 – CLHS: Section 6.1.6