Lispのカッコは怖くないよ

最近Lispの連れション仲間を増やしたいので、いろんな初見の人に「Lispって知ってる?」と質問して回っています。

そこそこアンテナのある技術者ならLispというのがプログラミング言語の一派を意味しており、それが主に大量のカッコで構成されていることは知っているようなのですが、なんか拒否反応が多いんですよね。

拒否反応というのが、まあ、だいたい

  • 「カッコが多すぎて気がおかしくなる」
  • 「私の人生は大量のカッコに対応するには短すぎる」
  • 「大学でやったけどカッコ死ね。」

みたいなHTML初心者がタグのネストに敗北したみたいな感想ですね…。

まあ、パッと見てそういいたくなる気持ちは分かるんですが、それ自体がよくあるLispに対する誤解と言わざるを得ないです。

事実、Lispプログラミングは大量のカッコを相手にするのですが、誰もカッコの個数なんて見ていません。

Lispのプログラムの構造を読むときは、インデントしか見てません。

カッコを読む側の話

Lispプログラムの例として、下記のregexp-opt-depthという関数(Emacsregexp-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プログラムの構造は、構文上の絶対の規制はないものの、インデントで視覚的に示します。考え方はPythonHaskellCoffeeScriptと同じですね。JavaやCでもまともなプログラムなら構文に沿ってインデントをつけるはずです。

カッコを作る側の話

で、インデントで構造を示すのはいいんですが、インデントなぞただの努力目標、本質的にプログラムの構造はカッコの対応で示されます。(意地悪でカッコに反するインデントをすることも可能です。)

海千山千のLISPerはこれら大量のカッコをいちいちバランスとれるように開きの数と閉じの数を数えながら、Lispプログラムを編集しているのかというと、もちろんNoです。エディタの機能でどうにかします。

普通はpareditというカッコのバランスとったままカッコ単位の各種編集ができる便利なEmacsマクロを使います。これによりほとんどカッコの対応を崩さずに編集可能になります。逆に言うとこれがないとLispなんてやってられません。

Lispではすべてがカッコなので、pareditで好きな単位の式を綺麗に抜き取ることも可能です。

カッコ単位の編集に特化している感じですね。

カッコになっていれば、全部同じ方法で編集できるので、Lisperにとってすべてがカッコの世界はとてもハッピーかもしれません。

まとめ

カッコは怖くないよ!!

2014-08-05 追記

すごいブックマークついてる!!

Lispのカッコよりみなさんの忌憚なきコメントのほうがLisp初心者の僕にとっては怖いですね。

nazoking ((はてなブログで((((gyazo)に置かれた(gifアニメ)のURL)をツイッターでシェア)したツイート)を貼り付け)していて、結果gifアニメが動いていないという悲劇

すんません次からはちゃんとやります

Lispの魅力伝えられてないよ

まず、カッコごときでLispのことを食わず嫌いにならないで欲しいと思いました。

マクロとかは後でいいかと。できればハッカーと画家でも読んでもらえれば。

countの後ろのかっこの数

これは普通にカッコの釣り合いをとってくれるエディタで書いていけば、開き-閉じかっこの数なんかはずっと調整が取れている状態になり、気にしなくてもちゃんとなるようになってます。

Pythonとかでいいじゃん

今回書いたのは単にLispのカッコを恐れる必要がない理由だけですので、PythonLispと同じ土俵に立っているわけではありません。

マクロ機能はもちろん、各Lisp系言語ごとの目玉機能も違いがありますからね。

Lispの利点は他の言語で構文木取り扱う場合にASTパーサ持ち出したり、プリプロセッサだの普段使わない道具を持ってこざるをえなかったりしなきゃいけないといったことがないことです。以前のC++のように>>がテンプレート構文としてダメだどうかとかもありません。これはひたすらカッコしかないお陰です。

ひたすらカッコしかないお陰で、カッコ編集機能だけでいろんなLisp系言語(Common LispSchemeClojure、EmacsLisp)をカバーできたりします。これは他の言語には真似できないところでもあるし、永遠にカッコから脱却できない理由でもあるんじゃないでしょうか。

まあ、本質的には木構造的な何かが表せられればいいので、別にカッコじゃなくてもいいと思います。(編集は面倒そうだが)

emacs

今回はEmacsのpareditを紹介しましたが、vimや他IDEでもたぶんまともに編集できる環境はあるはずです。

LOL(let over lambda - http://letoverlambda.com/ )の人はvimで編集しているらしいです。

Clojure

にゃんぱすー!

tanimina: cf. 「S式の読みやすさ」http://blog.practical-scheme.net/shiro/20120823a-s-expression

このエントリの意味ないやん…

Shiroさんのサイトには全ての答えがありますね…

読み方がわかった、怖くなくなった、なるほど など

あなたたちを待っていました!

ほかみなさん

コメントありがとうございました。

ちなみに、私はもっぱら何か考える時はCoffeeScriptで考えます。

ま、普通に 1+2 とか i++ とか書きたいし、関数は (x) -> (y) -> x + y とかすっきり書けるし、分配もあるし、リスト内包記法みたいなのもあるし?

しかしCoffeeScriptでインデントがとち狂い始めた時、Lispのほうが私の木構造への意図がコードに残り続るし、カッコの編集楽でいいなあと思ったりします。

っていうかいろんな言語がわざわざASTとか作んないでさあ、S式に一度なってくれればいいんだよねー。