Edited at

Lispってどう書くの?

More than 1 year has passed since last update.


はじめに

ここではLispの文法やらREPL駆動開発についての云々は語りません。

「あの()だらけのプログラムってどうやってタイプしていったらいいの?」

という話をします。

目新しい情報ではないのですが、知らない方もいらっしゃるのでご紹介できればと思います。


まずはLispの読み方から

読めないものは書けない、ということで。

よく言われる話ですが、Lispでは基本的に()ではなくインデントに着目してコードを読みます。Pythonが如く。

この件については Lispのカッコは怖くないよ より「カッコを読む側の話」をご覧いただくのが分かりやすいと思います。

ちなみに、この記事は上記記事の「カッコを作る側の話」の内容を少し突っ込んだものとなっています。


書き方の概要

以下のような、ありふれた階乗の関数を書くとしましょう。

(defun fact (x)

(if (<= x 1)
1
(* x (fact (- x 1)))))

これを皆さんはどのように書くでしょうか?

恐らくですが、慣れていない方は)を正しい個数付与するのに四苦八苦するのではないでしょうか。

これは一例でしかありませんが、以下のGIFアニメーションのような書き方をします、というのがこの記事の趣旨となっています。

ざっくりとしたLispの書き方

<=と表示されるのは趣味です

いかがでしょうか。言葉で説明するよりも動画でご覧いただいた方が伝わりやすいかと思ったのでいきなりこちらをお見せしました。

エディタ、プラグイン、あるいはIDEの機能、何でもいいのですが()の対応を崩さずに記述をしていける機能があるのでそれを使っていくことになります。


()のプラグインって?

ParEditという伝統的な()操作プラグインがあるので、それが用いられることが多いようです。この記事の用語はそれに合わせています。

ParEdit本家はEmacsですが、他のメジャーなエディタにも同プラグインが存在しているようです。

なお、ParEditよりも新しいプラグインも出てきていますので、記事の最後の方でまとめておきます。

一方でエディタにしてもプラグインにしても、あるいはIDEを使うにしても、手に馴染むものを選べば良いというスタンスで進めたいと思います。

この記事の趣旨はツールの推薦ではなく「()を快適に記述する概念の紹介」です。

なお、インデントをよしなにしてくれるプラグインは導入済みのものとします。


()編集の基本


()は開くと同時に閉じる

([{"'等を開いたら自動で閉じてくれる機能がエディタに備わっていることは珍しくないかと思います。

中にはこれを「余計なお世話だ」としてOFFにする方もいるのではないでしょうか。

しかしこと(については、それを捨てるなんてとんでもない! のがLispプログラミングです(Clojureとなると[{も自動で閉じて欲しいところです)。

()を閉じるのは人間がわざわざやることではありません。

自動的に閉じるんです。つまり、(をタイプしたら自動的に)が入力される状態となることで、常に()の対応関係が維持されるようにします。

これが「Lispの)の個数を数えて一致させなくてもよい」状態に近づく始めの一歩です。

ちなみに「()の中身を先に書いてから()を書きたいので、自動で)が付与されると邪魔」という方もいらっしゃるようですが、その場合は()で囲いたい部分のリージョン選択してから(をタイプすれば良いです。

選択範囲の末尾に)が付与される筈です。たぶん。

括弧を後から付ける


ネストが深くなってきたら

それでも、ある程度ネストが深くなってくるとどの階層に新たな式を追加したら良いかよく分からなくなってくる時もあるでしょう。

;; 例えば

(list (* 1 2)
(* 3 4)
(* 5
(+ 6 7))) ; <- このリストの末尾に要素を追記したい

追記したい要素は、何でもいいんですが、ちょっと煩雑でこんなものだったとしましょう。

(mapcar (lambda (x) (* x 2))

(reverse '(1 2 3)))

この様な場合は、無理に既存の()内にネストさせようとせず、まずは分かりやすい階層に書いてしまいます。

(list (* 1 2)

(* 3 4)
(* 5
(+ 6 7)))
(mapcar (lambda (x) (* x 2))
(reverse '(1 2 3)))

そして、エディタなりの機能を使って、これをあるべき階層まで運びます。

飲み込み操作

lambdaλと表示されるのは趣味です

これを「飲み込み(slurp)」と言います。

隣接する()に、別の式を飲み込ませることで、()の対応関係を保ったままプログラムを編集することができます。

ところで、飲み込ませ過ぎて意図しない階層まで式が潜り込んでしまった場合にはどうしたらいいのでしょうか。

(list (* 1 2)

(* 3 4)
(* 5
(+ 7 8)
;; しまった、ネストさせ過ぎた
(mapcar (lambda (x) (* x 2))
(reverse '(1 2 3)))))

この場合、「吐き出し(barf)」という飲み込みの逆操作を使って式を追い出します。

吐き出し操作

なお、適切な階層まで移動できたか否かの判断は、インデントを見て行います。


もうちょっと別の操作


()が不要だったと気付いたら

例えばです。

;; こう書いたんだけど

(apply #'+ '(1 2 3))

;; 実はこれで十分だと気付いてしまった場合
(+ 1 2 3)

先程の吐き出し(barf)を地道にやっても修正することはできますが、いささか面倒でしょう。

jimichi_barf.gif

ここで使うのが接合(splice)という操作となります。

これは、(およびそれに対応する)を同時に削除します。そしてより上位の()に接合されることからこの名前が付いているのでしょう。

接合操作


コピペをしたいなら

最初の階乗の例に戻ります。

(defun fact (x)

(if (<= x 1)
1
(* x (fact (- x 1)))))

これを、条件を逆にして以下のように書き換えたいという気持ちに駆り立てらてたとしましょう。

(defun fact (x)

(if (> x 1)
(* x (fact (- x 1)))
1))

通常のコピペ(ここではコピーではなくカットになりますが)の感覚でいくと、最初の状態において「末尾の)を何個選択すればよいのだろう?」、と混乱してしまうかもしれません。

しかし、ここでも)を気にする必要はありません。

以下の様に、()の塊の単位でコピーやカットをすることができます。

※こちらの例ですと()の置換(transpose)で一発であるとのご指摘をいただきまして、何か良い別の例を思い付いたらリプレイスします。

カットしてペースト

もちろん、コピー・カット対象の途中に改行が含まれていても大丈夫です。

カットしてペースト(改行あり)


コメントアウト

愚直に;とタイプしてしまうと余分な)も巻き添えを食らってコメントアウトされてしまい、()の対応関係が崩れてしまうケースも多々あります。

そんな場合でも必要な部分だけコメントアウトすることが可能です。

comment_out.gif


もっと詳細に知りたい

以下にParEditの詳細な日本語のチュートリアルがありますので、興味を持たれた方は取っ掛かりとしてご覧いただくのが良いかと思います。

ParEdit チュートリアル

こちらはParEdit以外をお使いの方にとっても、概念を把握できるという点で有益な内容かと思います。

さらに踏み込んだところとなりますと、やはりお使いのツールの公式ドキュメントをご覧になるのが良いかと思います。

ツールについては後述いたします。

SmartparensについてはGitHub上のWikiを地道に眺めるのも良いですが、手っ取り早い把握手段として こちら の"keybinding management"でキーバインドの定義されている操作をざっと把握してみるのが良いかも知れません。


おまけ


()のためのツール

コメント欄にていくつか紹介していただけているので、折角ですからこちらで簡単にまとめておきたいと思います。

「他にこんなのがあるよ!」という場合には、コメント欄や編集リクエストにてご指摘願います。

エディタやIDEの種類は問いません。


何で)を全部揃えて書くの?

C言語等の{}のように扱えればもっと楽なのでは、そう考えたことはありませんか?

つまり)を別の行に書くことです。

;; 大雑把に言うとこんな感じ

(defun fact (x)
(if (<= x 1)
1
(* x (fact (- x 1)))
)
)

C言語等において{}の対応関係で困ったことはあまりないのではないでしょうか(憶測ですみませんが)。

これならば末尾に新たな要素を追加するのも容易になるのではないかと考える方もいそうです。というか、自分は当初このスタイルで書いてからわざわざ)をくっつけていました。

(そして)をくっつけるエディタプラグインを書こうかと思った矢先にこの記事のようなスタイルにシフトしました)

これについてはStack Overflowでの議論 Lisp Parentheses にもありますが、Lispにおいては前述の通りインデントで読んでいくため、)が他の行にあったとしても読解上のメリットがなく冗長なだけであるためと理解しています。

加えてこの記事で紹介したような手法によって記述上のデメリットが消えるので、)を他の行に書く必要性が生じません。