- EmacsでCommon Lispのloopをネストすると、appendの位置に次のloopが揃えられ、インデントが深くなる問題が起きる。
- Emacsのインデントは「引数揃え」「ボディインデント」「lisp-indent-functionプロパティ」の3ルールで決まる。
- Common Lisp固有の変数
lisp-loop-indent-subclausesがloopサブ句ごとのインデントを制御していて、デフォルトのtが深いインデントの原因になる。 - lisp-modeなら
putでインデント関数に整数を指定し、SLIME環境ならlisp-loop-indent-subclausesをnilにすることで浅いインデントに切り替えられる。
1. Loopのオートインデントが深い?
Emacsのlisp modeのオートインデントが深すぎる気がします。
3つのloopを組み合わせたコードを書きました。

しかし、これをEmacsでオートインデントすると、かなり深いインデントになってしまいました。

(defun triples (a b c)
(loop for i from 0 to a append
(loop for j from 0 to b append
(loop for k from 0 to c collect
(list i j k)))))
Code language: Lisp (lisp)
これは、2つの目のloopがappendの位置に揃えられてしまっていることが原因です。
1.1. loopのサブ句を先頭に出す
Common Lispでは、loopのサブ句(for や append など)をインデントに評価します。
そこで、appendの位置で改行するようにすると、読みやすくなります。
(defun triples (a b c)
(loop for i from 0 to a
append (loop for j from 0 to b
append (loop for k from 0 to c
collect (list i j k)))))
Code language: Lisp (lisp)

1.2. SchemeとCommon Lisp
このような構文が生じるのは、Common Lispに特殊な句構造が入り込んでいるからです。
別のLisp方言である Schemeなら、append-mapとlambdaを並べる書き方が自然です。
(import (srfi 1)) ; R7RS スタイル
(define (triples a b c)
(append-map (lambda (i)
(append-map (lambda (j)
(map (lambda (k) (list i j k))
(iota (+ c 1))))
(iota (+ b 1))))
(iota (+ a 1))))Code language: Lisp (lisp)
loop appendがappend-mapに、for が iota に相当しているわけですね。
もちろん、Common Lispでも、loopを使わずに書けますが、抽象的で慣れが必要です。
(defun iota-range (n)
(labels ((rec (i acc)
(if (< i 0)
acc
(rec (- i 1) (cons i acc)))))
(rec n '())))
(defun triples (a b c)
(mapcan (lambda (i)
(mapcan (lambda (j)
(mapcar (lambda (k) (list i j k))
(iota-range c)))
(iota-range b)))
(iota-range a)))Code language: Lisp (lisp)

S式としては自然でインデントも素直に入りますが、loopの実用的な読みやすさも魅力です。
2. EmacsのLispインデントの3つのルール
Emacs の Lisp インデントは、S式の構造と関数の種類に応じて3つのルールを使い分けています。
- 引数揃え
- ボディインデント(
defun、defmacro、defvarなど) -
lisp-indent-functionプロパテ
2.1. 関数呼び出しの最初の引数
ルール1は「引数揃え」です。
関数呼び出しの最初の引数が関数名と同じ行にある場合、2行目以降はその引数の位置に揃います。
(some-function arg1
arg2
arg3)
Code language: Lisp (lisp)
some-function が長いほど右にずれます。
これが「深すぎる」と感じる原因です。
最初の引数が次の行に落ちている場合は、関数名の直下に揃います。
(some-function
arg1
arg2
arg3)
Code language: Lisp (lisp)
開き括弧から2カラムの位置です。
2.2. 定義のボディ
ルール2は「ボディインデント」で、def で始まる名前のフォームに適用されます。defun、defmacro、defvar などが該当し、最初のいくつかの引数は特別扱いされ、残りの「ボディ」部分は開き括弧から lisp-body-indent(デフォルト2)カラム下がります。
(defun hello (name)
(message "Hello, %s" name)
(do-something))
Code language: Lisp (lisp)
(message ...) の位置は defun の d から2カラム右です。引数 hello や (name) の位置には揃えません。ボディが始まるまでの「特別扱い」引数の個数は、フォームごとに lisp-indent-function プロパティで定義されています。
2.3. 個別制御(lisp-indent-function)
ルール3は lisp-indent-function プロパティによる個別制御です。
各シンボルにこのプロパティを持たせることができ、4種類の値を取ります。
整数 N を指定した場合、先頭から N 個の引数は4カラムインデントで「特別扱い」され、N+1 個目以降はボディとして lisp-body-indent でインデントされます。
;; when は lisp-indent-function が 1
;; 最初の1引数(条件式)が特別扱い
(when (some-condition)
(do-this)
(do-that))
Code language: Lisp (lisp)
defun を指定した場合は def 系と同じボディインデントが適用されます。lambda がこの扱いです。
(lambda (x y)
(+ x y))
Code language: Lisp (lisp)
関数シンボルを指定すると、インデント位置をそのつど計算して返します。if のように引数ごとに異なるインデントが必要なフォームで使われています。
プロパティが nil のシンボルには、ルール1の引数揃えがそのまま適用されます。通常の関数呼び出しはすべてこれに該当します。
Emacs はまず呼び出されているシンボルの lisp-indent-function プロパティを調べ、値があればそれに従い、なければ引数揃えにフォールバックします。
引数揃えは関数名の長さに比例してインデントが深くなるため、ネストが重なると右に張り出していきます。
ボディインデントは開き括弧基準の固定幅なので浅く収まる。
この二つの使い分けが、Lisp コードの視覚的な構造を作っています。
2.4. 【さらに】Common Lispでの繰り返し構文の扱い(lisp-loop-indent-subclauses)
Common Lispでは、特別な処理があります。
たとえば、for、when、collect、append のような loop のサブ句を字句として認識し、句の種類ごとに異なるインデント幅を計算するのですす。
cl-indent.el には loop マクロのために専用の変数として、lisp-loop-indent-subclauses が用意されています。
デフォルトでは、t に設定されていて、具体的には、クロージャ句(collect、append、do など)の本体はキーワードより深く下げ、継続行は前の句に揃えます。
;; lisp-loop-indent-subclauses が t のとき
(loop for i from 0 to 10
when (evenp i)
collect i
finally (return result))Code language: Lisp (lisp)
nil にすると、この句ごとの計算を停止して固定幅のインデントに切り替わります。loop を通常の関数呼び出しと同じように扱い、開き括弧からの一定幅で揃えます。
;; lisp-loop-indent-subclauses が nil のとき
(loop for i from 0 to 10
when (evenp i)
collect i
finally (return result))Code language: Lisp (lisp)
デフォルトが t なのは、Common Lisp の loop マクロが複数のサブ句を連ねる構造体として書かれることを想定しているためです。
句の対応関係が視覚的に追いやすくなるという意図があります。
3. インデント設定を変更するには
ネストした loop を含むコードでの右への張り出しが積み重なるが嫌なら、強制的に変更することができます。
まずは、自分のバッファがlisp-mode, cl-indent.el のどちらのインデント関数を使っているか確認します。
Emacsには、Lispのインデント関数が2つあるからです。
lisp-indent-functioncommon-lisp-indent-function
対象のファイルを開いた状態で次を実行してください。
M-x describe-variable RET
lisp-indent-function RET
Code language: JavaScript (javascript)


Its value is 'lisp-indent-function' と表示されれば標準の lisp-mode です。
一方、SLIME や sly を使っている環境では、'common-lisp-indent-function' と表示され、 cl-indent.el が有効になっているはずです。
3.1. lisp-mode(デフォルト)の場合
put でシンボルにプロパティを設定します。
(put 'loop 'lisp-indent-function 1)
Code language: Lisp (lisp)
整数 1 を指定すると、loop の最初の引数(for)が「特別扱い」の1つになります。
すると、2行目以降はボディとして開き括弧から lisp-body-indent(デフォルト2)カラムの位置に揃います。
(defun triples (a b c)
(loop for i from 0 to a append
(loop for j from 0 to b append
(loop for k from 0 to c collect
(list i j k)))))
Code language: Lisp (lisp)
init.el には次のように書きます。put はシンボルのグローバルなプロパティを変更するので、フックに入れず1回だけ書けば十分です。
(put 'loop 'lisp-indent-function 1)
Code language: Lisp (lisp)
3.2. common-lisp-indent-function(SLIME など)の場合
一方、cl-indent.el は loop を common-lisp-loop-part-indentation という専用関数で処理していて、lisp-indent-function プロパティを見ません。
そこで、2つの設定が必要です。
lisp-loop-indent-subclauses は loop のサブ句(for、append など)を意識した深いインデントを制御する変数で、nil にすると固定幅のインデントに切り替わります。put の方は common-lisp-indent-function プロパティに対して設定します。
;; シンボルプロパティはグローバルで一度だけ設定
(put 'loop 'common-lisp-indent-function 1)
;; バッファローカル変数はフックで設定
(add-hook 'lisp-mode-hook
(lambda ()
(setq-local lisp-loop-indent-subclauses nil)))
Code language: Lisp (lisp)
put はグローバルに効くのでトップレベルに置き、lisp-loop-indent-subclauses はバッファローカルにしておくと他のモードへの影響がないので setq-local をフック内で使います。
ファイル単位で設定したい場合は、ファイルの末尾にローカル変数として書く方法もあります。
;; Local Variables:
;; lisp-loop-indent-subclauses: nil
;; End: