2006年12月17日

Common Lisp: loopマクロ用法抄

GrahamのANSI Common Lispでは嫌われていて碌に説明のないloopマクロ。一方、Practical Common Lispでは対照的に好んで用いられていて、全編に渡って頻繁に使われている。しかしloopマクロは難しいという意識があるのかその説明は第22章とかなり後回しにされており、ちぐはぐな感を受ける。ここでは、LOOP for Black-Belts という題のつけられたその章で解説されているloopマクロの用法を整理してみた。

ANSI Common Lispでの黒魔術扱いに敬遠していたloopマクロだったが、こうして整理してみるとそれほど難しく考えずとも便利に使うことができそうだ。

Practical Common Lisp

目次

概要

一つ以上の繰り返し(for節)の後に、ループの中身であるアクションを一つ以上書く。アクションの主たるものは値の集約だが、任意の式の実行も可能。

繰り返しは複数個指定でき、その場合は入れ子ではなく同じループの中で並行して扱われる。

繰り返しの終了条件のどれかが満たされるとループは終了する。

アクションも複数指定でき、それらは毎回順番に実行される。

例: この繰り返しではxとyがそれぞれ並行して進む。先にxのリストが尽きるため、yはまだ残りがあるがループは3回で終了する。

(loop for x in (1 3 5)
      for y in (2 4 6 8 10)
      collect (cons x y))
==> ((1 . 2) (3 . 4) (5 . 6))

例: 一つのループで複数のアクション。一つめでyを更新し、二つめでloopの戻り値となる値に集約。

(loop for x in '(1 2 3 4 5)
      sum x into y
      collect y)
==> (1 3 6 10 15)

計数繰り返し

for 変数  [
from N
upfrom N
downfrom  N
[
to N
upto N
downto  N
below N
above N
[ by N ] 〜

次の3要素の1つ以上が必要:

from 数値 (始値の指定)
from N / upfrom N / downfrom N (デフォルトは0)
to 数値 (上限または下限の指定)
to N / upto N / downto N / below N / above N
by 数値 (増分の指定)
by N (デフォルトは1または-1)

デクリメント時の注意点:

  • 始値のデフォルトは存在しないため省略不可。
  • downfromdownto/belowによって増分の方向が負であることを明示する必要あり。(例えば、from 20 to 10等は不可。downfrom 20 to 10from 20 downto 10等とする)

例:

(loop for i from 10 to 50 by 5 collect i)
==> (10 15 20 25 30 35 40 45 50)
repeat N

N回繰り返し。カウンタ変数の値そのものは不要な時に。

コレクション内繰り返し

for 変数 
in リスト
on リスト
acrossベクタ
 [ by ステップ関数 ] 〜

ステップ関数は次の要素を保持する位置を得る関数で、 デフォルトは当然#'cdr。この関数次第でリストに限らず広く応用もできそうだ。

on リスト」の場合、変数には要素ではなくconsセルが代入される。

(loop for x in '(1 2 3 4) collect x) ==> (1 2 3 4)
(loop for x in '(1 2 3 4) by #'cddr collect x) ==> (1 3) ; 一つ飛ばし
(loop for x on '(1 2 3) collect x) ==> ((1 2 3) (2 3) (3))
(loop for x on '(1 2 3 4 5) by #'cddr collect x)
==> ((1 2 3 4 5) (3 4 5) (5))
for 変数 being the 
hash-keys
hash-values
 in ハッシュ表 [ using (
hash-value
hash-key
 変数 ) ] 〜
for 変数 being the 
symbols
present-symbols
external-symbols
 in パッケージ

ハッシュ表やパッケージの要素についての繰り返し。ハッシュ表ではループ変数はキーか値の一方について繰り返すがusingを使うことで他方も参照可能。

(defvar ht (make-hash-table))
(setf (gethash 'foo ht) 1)
(setf (gethash 'bar ht) 2)
(loop for x being the hash-keys in ht collect x) ==> (BAR FOO)
(loop for x being the hash-keys in ht using (hash-value y) collect (cons x y))
==> ((BAR . 2) (FOO . 1))

変数更新しながら繰り返し

for 変数 = 式1 [ then 式2 ] 〜

最初のループでは式1を評価した結果を変数の値とし、以降のループでは式2(省略された場合は式1)を評価した値で更新する。doマクロとは異なり、式2が省略された場合も式1を毎回評価して変数を更新する。

複数の変数を更新する場合、forを複数並べるか、andを使う。

(loop repeat 5
      for x = 0 then y	        ← 前回のyが使われる
      for y = 1 then (+ x y)    ← 新しいxが使われる
      〜)

(loop repeat 5
      for x = 0 then y          ← 前回のyが使われる
      and y = 1 then (+ x y)    ← 前回のxが使われる
      〜)

ループ終了条件の追加

while
until
 

が偽になったら(untilの場合は真になったら)ループは終了。

(loop for y = nil then (cons 'x y) while (< (length y) 3) collect y)
==> (NIL (X) (X X))

集約

collect
append
nconc
count
sum
maximize
minimize
  [ into 変数 ]

式を評価しその結果をそれぞれの方法で集約する。

into句があると、集約の結果はループ内ローカルな変数に代入される。ない場合は集約の結果はloopのデフォルトの戻り値となる。

動詞はそれぞれ〜ingという動名詞形でもよい。

  • collect: 式を並べたリストを作る
  • append: 式をリストとみて、それらを結合したリストを作る
  • nconc: 〃
  • count: 式が真となる場合の回数を数える
  • sum: 式を足し合わせる
  • maximize: 最大値を選ぶ
  • minimize: 最小値を選ぶ

ループ内ローカル変数

with 変数 [ = ]

任意の式の実行

do [ ... ]

任意のLisp式を毎回実行。別のloopマクロ内キーワードが出現するかloopマクロの終わりまで任意個のLisp式を続けられる。

条件分岐

if
when
unless
 条件 〜 [ else … ] [ end ]

条件は任意のLisp式。ifwhenは同義。

〜や…の部分はloopマクロの節。複数の節を入れたい場合はそれらをandで繋ぐ。

endは省略可。

(loop for x from 1 to 5 if (evenp x) collect x) ==> (2 4)

ループ中断

return
named ラベル for ...

returnはループを中断する。finally節は実行されない。ループから脱出したいが正常終了と同様にfinally節を実行したり値を返したい場合、do節の中でLOOP-FINISHマクロを用いる。

namedはループが作るブロックに名前をつける。その名前を使ってRETURN-FROMしてもループを抜けられる。

(loop named outer for xs in lists do
      (loop for x in xs do
            (if (foop x) (return-from outer x) ...)))

繰り返しの初期化・後始末

initially
finally
  [ ... ]

どちらもdo同様に次のloopキーワードまで任意個の式が書ける。

finally節が実行されない3条件:

  • return節で脱出した場合
  • RETURN, RETURN-FROMで脱出した場合 (つまり、finallyunwind-protect で実現されるわけではない)
  • ループが always, never, thereis 条件で終了した場合

不変条件

always
never
thereis
 

ループの全繰り返しに渡って不変条件が守られたかをloopの戻り値とする。条件が破られるとループを中断、finallyによるエピローグもスキップする。

中断する条件中断時の戻り値完了時の戻り値
always 式が偽になったら中断 nil t
never 式が真になったら中断 nil t
thereis式がnil以外になったら中断式の値 nil

分割代入

記述力はdestructuring-bindに劣るものの、forwithによる変数への代入は、リストやconsの分割代入が可能。

値を無視したい部分にはnilを使う。

for (a b) in '((1 2) (3 4) (5 6)) 〜
for (kar . kdr) on '((1 . 2) (3 . 4) (5 . 6)) 〜

参考


この記事へのトラックバック
この記事へのコメント
コメントを書く
お名前: [必須入力]

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。