- Common Lisp の
loopは、キーワードを組み合わせて柔軟にループを記述する構文です。 forやrepeatの回数指定や、collect、sum、countでの集計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) で各ステップの値を加算し、値として返します。listとsumの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 に束縛します。
インデックスを気にせず書けるので、コレクションを処理するときは from と to より自然な選択肢です2。
1.5. 条件に合うものだけ(when / unless)
ループ中で条件に応じて処理を分けるには、when と unless を使います。
when / unless の後には、collect や sum、do などの節を続けることができます。
条件に応じたフィルタ処理を簡潔に表現できます。
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が使われます。
また、fromとtoにも、 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)
while と until との大きな違いは、これらが値を返しながら早期終了する点です。while と until は繰り返しの継続を制御するだけで、値は返しません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。for、collect、sum といったキーワードは、通常の 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, finally)
for-as節 ::= for var [of-type type]
(from-to-spec | in-spec | ...)Code language: JavaScript (javascript)
{変数節}* は変数節を何個でも並べられることを意味します。{メイン節}* も同様です。[named name] は省略可能で、省略すると暗黙のブロック名 nil が使われます。
| 種類 | 節 | 備考 |
|---|---|---|
| 変数節 | for(as)、with | for と as は同義語 |
| 無条件実行節 | do(doing)、return | |
| 値蓄積節 | collect、append、nconc、sum、count、maximize、minimize | 各節に -ing 形の別名あり(collecting など) |
| 終了条件節 | repeat、while、until、always、never、thereis | for/as の範囲指定も終了条件を持つ |
| 条件節 | if、when、unless、else、end | else/end は条件節の構成要素 |
| 初期化・終了節 | initially、finally | 変数節にもメイン節にも書ける |
3.2. マクロ展開と実行順序
拡張 loop のマクロ展開後の形は
- prologue(初期化処理)、
- body(各イテレーション)、
- epilogue(終了処理)
の3部で構成されます。
initially は必ずループ開始前に、finally はループの通常終了後に実行されます。
それ以外の節は書いた順に実行されます。
ただし、for 節の初期化は with より前に行われます。
4. 反復の前後に関する節
4.1. named と return-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章で見た collect と sum 、count のほかにもいくつかあります。
| 節 | 動作 |
|---|---|
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)
nconc は append と似ていますが、引数のリストを破壊的に変更して連結します。
新しいコンスセルを作らない分だけメモリ効率がよい場合がありますが、元のリストが変更されるため、リテラルリスト('(1 2 3) のような式)に対して使うと動作が未定義になります。13
6.1. 複数の蓄積節と into
通常、蓄積節は一種類しか使えません。
たとえば、collect と sum をそのまま並べると、どちらの値を返すか定義されていません。
そこで、sumやcountなどで複数の蓄積結果が必要なときは必ず 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)
ループ本体の中で条件分岐するには when、unless、if を使います。
文字列から数字だけを抽出する例です。
; 数字文字だけ取り出す
(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で振り分ける
if は else と組み合わせて、偶数と奇数の和を同時に求める例です。
(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
when、unless、if の後には蓄積節や無条件実行節を続けられます。
ネストは 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関数を使ってもよさそうです。
loopマクロは展開後、内部的にblockとtagbodyを含む形になります。戻り値はそのblockから返されます。 – CLHS: Section 6.1.1.4 Expanding Loop Formsforとasは完全な同義語で、どちらを使っても動作は同じです。文体的な好みで使い分けられます。 – CLHS: Section 6.1.2.1- ただし、
finallyは、常に実行されるとは限らない点には注意が必要ですalways、neverやthereisによる早期終了では実行されません。ループ本体の深い場所から通常終了と同じ手順で抜けるには、loop-finishというローカルマクロが使えます。loop-finishはfinally節を実行してから終了します。 – CLHS: Local Macro LOOP-FINISH for、as、repeatといった変数節は、他の節(do、collectなど)より前に書かなければなりません。ただしinitially、with、namedは例外で、変数節と混在できます。 – CLHS: Section 6.1.2.1- 数値範囲の指定には
to(終端を含む)、below(終端を含まない)、above(逆方向で終端を含まない)、downto(逆方向で終端を含む)があります。 – CLHS: Section 6.1.2.1 Iteration Control always、never、thereisは内部的にreturnを使って終了するため、finally節が実行されません。一方、whileとuntilはループの後始末(epilogue)に制御を渡すため、finally節が実行されます。 – CLtL2: Section 26.7 End-Test Control- simple loop と extended loop の違いは HyperSpec の Macro LOOP のページで説明されています。
loop-finishは simple loop 内では使えません。 – CLHS: Macro LOOP loopマクロが展開されると、変数の束縛にletまたはlambdaに相当する形が使われます。初期値の計算は書いた順に行われますが、束縛のタイミングは実装依存の場合があります。 – CLHS: Section 6.1.1.4- BNF の完全な定義は HyperSpec の Macro LOOP のページで参照できます。仕様の疑問が生じたときの最終的な参照先は HyperSpec の第 6.1 節です。 – CLHS: Macro LOOP
initiallyの処理は prologue(前処理)として展開され、変数の初期化処理よりも後、最初のイテレーションよりも前に実行されます。 – CLHS: Section 6.1.1.4- 複数の
for節をandでつなぐと、変数の更新が並列に行われます。andなしで並べると逐次的に更新されます。 – CLHS: Section 6.1.2.1 - 分配束縛はネストしたリスト構造にも対応しています。たとえば
for ((a b) c) in ...のように書くと、各要素のサブリストの先頭2要素と残りを別々に受け取れます。 – CLHS: Section 6.1.1.7 Destructuring nconcは最後の引数以外のリストの末尾 CDR を書き換えます。同じリストを2度nconcに渡すと循環リストになります。 – CLHS: Function NCONC- 各蓄積節には
-ing形の別名があり、collect/collecting、sum/summing、count/countingのように使えます。動作の違いはなく、文体的な好みで選べます。 – CLtL2: Section 26.8 Value Accumulation itはloopの条件節に固有の擬似変数で、when/unless/ifのテスト式の評価結果を保持します。ループ内でITという名前の通常変数を使うと、この擬似変数と衝突する可能性があります。 – CLHS: Section 6.1.6