最近Lispの連れション仲間を増やしたいので、いろんな初見の人に「Lispって知ってる?」と質問して回っています。
そこそこアンテナのある技術者ならLispというのがプログラミング言語の一派を意味しており、それが主に大量のカッコで構成されていることは知っているようなのですが、なんか拒否反応が多いんですよね。
拒否反応というのが、まあ、だいたい
- 「カッコが多すぎて気がおかしくなる」
- 「私の人生は大量のカッコに対応するには短すぎる」
- 「大学でやったけどカッコ死ね。」
みたいなHTML初心者がタグのネストに敗北したみたいな感想ですね…。
まあ、パッと見てそういいたくなる気持ちは分かるんですが、それ自体がよくあるLispに対する誤解と言わざるを得ないです。
事実、Lispプログラミングは大量のカッコを相手にするのですが、誰もカッコの個数なんて見ていません。
Lispのプログラムの構造を読むときは、インデントしか見てません。
カッコを読む側の話
Lispプログラムの例として、下記のregexp-opt-depthという関数(Emacsのregexp-opt.el)を見てください。
(defun regexp-opt-depth (regexp) (save-match-data (string-match regexp "") (let ((count 0) start last) (while (string-match "\\\\(\\(\\?[0-9]*:\\)?" regexp start) (setq start (match-end 0)) (when (and (not (match-beginning 1)) (subregexp-context-p regexp (match-beginning 0) last)) (setq last start) (setq count (1+ count)))) count)))
Javaなどであれば {
やら }
などなどもうちょっと可愛げのある記号が出てくるのに、特徴も糞もない大量のカッコとアルファベット+記号の羅列でめまいがしてきますね。
海千山千と言われるLisperはこのカッコの対応を一瞬で読み解ける超人ばかりなのでしょうか?
もちろんNoです。
上記のようなLispプログラムを読む場合は、普通は下記のように思考しながら読んでいきます。(各行の;; 以降が思考内容です。)
;; defunがあるから、関数定義だなあ。 ;; その次には関数名がくるはず。regexp-opt-depthという関数の定義をするんだなあ。 ;; regexpという引数一つをとるんだなあ。 (defun regexp-opt-depth (regexp) ;; ネストされたところからカッコが始まっている。 ;; 関数の処理の本体がここから始まるんだなあ。 ;; save-match-dataを使っているんだな。 (save-match-data ;; さらにネストされた位置にstring-matchの呼び出しがあるなあ。 (string-match regexp "") ;; string-matchが終わったらletを呼び出すんだなあ。 ;; letは変数の定義だからcountが0、startとlastは定義だけしてるんだなあ。 (let ((count 0) start last) ;; 変数の定義(let)の中にwhileの呼び出しがあるなあ。whileはループ文だなあ ;; (string-match… 部分がループ条件か。 (while (string-match "\\\\(\\(\\?[0-9]*:\\)?" regexp start) ;; こっからwhileのループ処理本体だなあ。 ;; setqは変数代入だなあ。start変数の書き換えかあ。 (setq start (match-end 0)) ;; ループ処理の最後にwhenで条件を満たした場合のみの処理を書いているなあ。 ;; インデント的に(and (not …) (subregexp-context-p ...))が条件か。 (when (and (not (match-beginning 1)) (subregexp-context-p regexp (match-beginning 0) last)) ;; 条件満たしたらsetqを2つやるんだなあ。 (setq last start) (setq count (1+ count)))) ;; インデントから見て、letの直下の処理で、最後にcountを返却しているんだなあ。 count)))
上記の読み方のどこにも遠く離れたカッコの個数や対応を数える兆候が見られないことに注目してください。
カッコの対応を知るのにカッコの数ではなく、(
が出現するインデント位置しか見ていません。
(局所的に近場のカッコの1つ2つは数えています。ただそれぐらいは通常の人間ができる芸当の範疇だと思います。)
このようにLispプログラムはインデントでしか読まないのが当たり前なので、逆にLispプログラムを書く場合はインデントで構造が読めるように配置しなければ相当読みづらいといえるでしょう。(普通はエディタが勝手にインデントとってくれます)
開きカッコのインデント位置は気にしますが、閉じカッコといえば、上記のインデントを守っていればただの開きカッコのつじつま合わせにしか過ぎないので、一箇所に集められるだけ集められているのがわかるでしょうか? count))))
などとなっている部分がそうですね。
このようにLispプログラムの構造は、構文上の絶対の規制はないものの、インデントで視覚的に示します。考え方はPythonやHaskell、CoffeeScriptと同じですね。JavaやCでもまともなプログラムなら構文に沿ってインデントをつけるはずです。
カッコを作る側の話
で、インデントで構造を示すのはいいんですが、インデントなぞただの努力目標、本質的にプログラムの構造はカッコの対応で示されます。(意地悪でカッコに反するインデントをすることも可能です。)
海千山千のLISPerはこれら大量のカッコをいちいちバランスとれるように開きの数と閉じの数を数えながら、Lispプログラムを編集しているのかというと、もちろんNoです。エディタの機能でどうにかします。
普通はpareditというカッコのバランスとったままカッコ単位の各種編集ができる便利なEmacsマクロを使います。これによりほとんどカッコの対応を崩さずに編集可能になります。逆に言うとこれがないとLispなんてやってられません。
Lispではすべてがカッコなので、pareditで好きな単位の式を綺麗に抜き取ることも可能です。
GIF by Gyazo http://t.co/xkCJA6St4y #Gyazo
— (call me #'knjname) (@knjname) 2014, 8月 4
カッコ単位の編集に特化している感じですね。
カッコになっていれば、全部同じ方法で編集できるので、Lisperにとってすべてがカッコの世界はとてもハッピーかもしれません。
まとめ
カッコは怖くないよ!!