- Common Lispの閉じ括弧は、慣れると意識に上らなくなる。
- 前置記法では開き括弧の直後のシンボルが評価規則を決めるため、意味は開き側に集中している。
- 閉じ括弧はフォームの終端を示すだけで、構造の読み取りはインデントが担っている。
- defunをトップレベルに並べるスタイルにより、括弧の対応をファイル全体で追う必要がない。
1. 閉じ括弧はほぼ見ていない
Common Lispをはじめて見た人は、括弧の多さにびっくりするものです。
特に、関数の末尾には大量の閉じ括弧が連なります。
(defun frequencies (M A)
(let* ((result (make-array M
:element-type 'fixnum
:initial-element 0)))
(loop for x fixnum in A
do (incf (aref result (1- x)))
finally (return result))))Code language: Lisp (lisp)
しかし、ちょっと慣れると、その感覚は変わります。
EmacsでCommon Lispを書いていると、開き括弧と閉じ括弧で意識の使い方がまるで違うことに気づきます。
開き括弧を書くときは、判断が伴います。(defunと打つか、(letにするか、(mapcarで行くか。
でも、閉じ括弧はそうではありません。
Lispに慣れてくると、閉じ括弧はほぼ見なくなります。
コードを書く時も、私のエディタ設定では smart-paren 機能で、自動的に閉じ括弧は補完されます1。
もちろん、長い関数だと途中で増減していることがあるので、最後は 対応括弧の強調表示や rainbow-paren で色の対応を確認して、必要な数を調整するぐらいです2。
どの閉じ括弧が何を閉じているかをいちいち考えません。
コードを読むときは、基本的に括弧は釣り合っている前提で読んでいます。
インデントを目で追えば構造はわかるし、コンパイルが通れば数は合っています。
うまくいっているときは存在に気づきません。
コンパイルエラーが出たときだけ、「あ、閉じ括弧が一個足りないな」と意識に上がってくる。
普段は透明です。
2. 前置記法のオペレーター
この非対称性には理由があります。
慣れだけではなく、構造の問題でもあります。
S式は前置記法をとります3。(+ 1 2)のように、演算子が先に来る。
この形式では、開き括弧の直後のシンボルがフォーム全体の評価規則を決めます。
(if condition then-form else-form)
(let ((x 1)) body)
(mapcar #'func list)Code language: Lisp (lisp)
(ifは第二引数か第三引数のどちらか一方しか評価しません4。(letは束縛を作ってからbodyを評価する。(mapcarはリストの各要素に関数を適用します。
同じ「関数呼び出しの形」に見えますが、評価のされ方はまったく違います。
その違いを宣言しているのは、すべて開き括弧の直後のシンボルです。
一方、閉じ括弧はそのフォームの射程を区切るだけで、自分では何も宣言しません。)という記号は「ここで何かが終わる」とは言っていますが、「何が終わるか」はスタックに積まれた開き側が知っています。
意味が開き側に集中しているから、閉じ側は透明になって当然なのです5。
3. 括弧の釣り合いを取る
(let* ((a (foo x))
(b (bar a)))
(baz a b))Code language: Lisp (lisp)
閉じ括弧が何個あるか数えなくても、字下げを見ればlet*がどこまで続くかわかります6。
閉じ括弧は視覚的な情報をほとんど持っていない。
インデントがその仕事を引き受けています。
3.1. トップレベルの関数スタイル
括弧の釣り合いを確認しやすいのは、Common Lispのスタイルとも関係しています。
オブジェクト指向言語でクラス定義がファイル全体を包むような構造とは違い、Common Lispではトップレベルにdefunを並べて書いていくのが基本のスタイルです。
各関数が一つのS式として独立して完結しています。
ファイルのどこを読んでいても「今どのスコープにいるか」を見失いにくく、括弧の対応をファイル終端まで追い続ける必要がありません。
- smart-parensはMatus Goljerが開発したEmacsのマイナーモードで、括弧や引用符などのペアを自動補完・管理します。autopair、paredit、electric-pair-modeなど複数のパッケージの機能を統合したものとして設計されました。MELPAからインストールできます。 – smartparens GitHub
- rainbow-delimitersはFanael Ringeardが管理するEmacsパッケージで、括弧・ブラケット・ブレースをネストの深さに応じて異なる色で表示します。Clojure、Emacs Lisp、Common Lispなど括弧が多い言語で特に効果があります。 – rainbow-delimiters GitHub
- S式(Symbolic Expression)はLispが1950年代末に導入した記法で、ネストしたリスト構造をそのままプログラムとデータの両方に使います。前置記法(prefix notation)は演算子を被演算子の前に置く形式で、
(+ 1 2)のように書きます。この形式によってすべての式が統一的な構造を持ちます。 – S-expression – Wikipedia - Common Lispの
ifは特殊オペレータ(special operator)として定義されており、通常の関数呼び出しとは異なる評価規則を持ちます。条件が真なら第二引数のみ、偽なら第三引数のみを評価します。このような「引数を選択的に評価する」動作は通常の関数では実現できないため、言語仕様として組み込まれています。 – Practical Common Lisp: Syntax and Semantics - インデントがコード構造を表現し、コンパイラが括弧の対応を検証することで、書き手は閉じ括弧の管理をツールに委譲できます。Lispのスタイルガイドも「コードの構造はインデントで読め」と一貫して述べており、括弧を数える読み方を推奨していません。 – A Guide to Formatting Lisp – Medium
- Common Lispのインデント規約では、閉じ括弧を単独の行に置かず、最後の引数や式の末尾に続けて書くのが標準とされています。EmacsのSLIMEやcl-indentライブラリがこの規約に基づいた自動インデントを提供します。インデントが構造を表すため、Lispに慣れたプログラマは括弧を数えるより字下げでコードを読みます。 – Indenting Common Lisp