【Emacs】
Common LispのLoopインデントが深い?
(lisp-loop-indent-subclauses)

  • 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を組み合わせたコードを書きました。

1. Loopのオートインデントが深い?

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

1. Loopのオートインデントが深い?
(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.1. loopのサブ句を先頭に出す

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つのルールを使い分けています。

Lispインデントの3ルール ① 引数揃え (func arg1 arg2 arg3) 関数名が長いほど 右にずれる ② ボディ (defun foo (x) (body1) (body2)) def系は固定2col ボディインデント ③ プロパティ lisp-indent- function 整数N defun 関数シンボル シンボルごとに個別制御 Emacs lisp-indent-function
  • 引数揃え
  • ボディインデント(defundefmacrodefvarなど)
  • 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 で始まる名前のフォームに適用されます。
defundefmacrodefvar などが該当し、最初のいくつかの引数は特別扱いされ、残りの「ボディ」部分は開き括弧から lisp-body-indent(デフォルト2)カラム下がります。

(defun hello (name)
  (message "Hello, %s" name)
  (do-something))
Code language: Lisp (lisp)

(message ...) の位置は defund から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では、特別な処理があります。
たとえば、forwhencollectappend のような loop のサブ句を字句として認識し、句の種類ごとに異なるインデント幅を計算するのですす。

cl-indent.el には loop マクロのために専用の変数として、lisp-loop-indent-subclauses が用意されています。
デフォルトでは、t に設定されていて、具体的には、クロージャ句(collectappenddo など)の本体はキーワードより深く下げ、継続行は前の句に揃えます。

;; 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 を含むコードでの右への張り出しが積み重なるが嫌なら、強制的に変更することができます。

インデント設定の変更方法 まず確認 M-x describe-variable → lisp-indent-function 値が lisp-indent-function → lisp-mode / common-lisp-indent-function → SLIME lisp-mode init.el に1行追加: (put ‘loop ‘lisp-indent-function 1) (loop for i … (loop for j … (loop for k … 2col ボディインデント SLIME / sly 2つの設定が必要: (put ‘loop ‘common-lisp-indent-function 1) (add-hook ‘lisp-mode-hook (lambda () (setq-local … nil))) putはグローバル setq-localはフック内で init.el / lisp-mode-hook

まずは、自分のバッファがlisp-mode, cl-indent.el のどちらのインデント関数を使っているか確認します。
Emacsには、Lispのインデント関数が2つあるからです。

  • lisp-indent-function
  • common-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.elloopcommon-lisp-loop-part-indentation という専用関数で処理していて、lisp-indent-function プロパティを見ません。
そこで、2つの設定が必要です。

lisp-loop-indent-subclausesloop のサブ句(forappend など)を意識した深いインデントを制御する変数で、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: