LISPUSER

LISPMEMOLisp is a programmable programming language. -- John Foderaro

(top)  (memo)  (rss)

括弧の理由 (1) Emacs による S 式編集支援

Lisp の括弧は良くネタにされます。実際、大量の過去は最初のインパクトは強烈だったのを覚えています。 いったいなぜ Lisper は括弧を捨てないのでしょう? 歴史的にみれば括弧を好まない Lisper も居ます。古くは CMU の AI リポジトリに CGOL という ALGOL ライクな 構文の Lisp リーダ/ライタがあり、これは S 式と相互に変換できました。CGOL で書いて、S 式で表示とかができたわけですね。

 ;;Execute by typing "cl < demo.txt".
 
 (load "parser.cl")
 (load "cgol.cl")
 
 #.(cgol)
 scripting $
 
 % C-style comments %
 % first some easy ones %
 1+1 $
 2*3/6 $
 % assignment %
 a:= 7 $
 b:= a*7 $
 
 % try out operator precedence %
 2*3/6 $
 100+3*5 $
 100*3+5 $
 % should be 47 % 2+2+6*7+7/7 $ 
 % floats use same arithmentic operators %
 2+95.3/7.25 $
 
 % can return value of arbitrary element in sequence %
 
 2+2 ; 3+3 ; 4+4 $
 2+2 & 3+3 ; 4+4 $
 2+2 ; 3+3 & 4+4 $
 
 eval $ % turn off evaluation to avoid undef'ed variables %
 % if's compose nicely %
 if (a<b) then
 if (a<c) then a
 else if (b<c) then b
 else c $
 
 % try some Lisp operators %
 [12,4,3,5,6] $
 a^b @c$
 \a,b,c; p;q;r $
 % escape into Lisp directly %
 !(make-array '(100 2) :element-type 'integer) $
 eval $
 4 isin [12,4,3,5,6] $

ほかにも Dylan などは S 式を捨てて ALGOL ライクな構文を導入することで普及を目指しました。 現在でも Lisp に S 式以外の構文を持たせようとする試みは Lisp を学ぶ者が誰でも一度は挑戦する定番の課題となりました 1 _ 。 comp.lang.lisp には毎年一度は確実にその手を話題が投稿されます。 「C/Python/Ruby ライクな構文シュガーを考えたんだ!!」 「C++ みたいにドットでメソッド呼び出しを書けるマクロをつくったよ!!」 などです 。

自分の経験だけでも、「メソッド呼び出しを C++ 風に(C/C++ から入ってきたので)」「インデントで構造を表す(Python の影響)」「後置記法に (forth の影響)」「区切記号の活用 ((f3 (f2 (f1))) -> f1 | f2 | f3 )」などやりましたが、どれも精々ユーザーインターフェースの一貫としてちょっと使ったくらいで、自分でプログラミングするための S 式を完全に置きかえるには至りませんでした 2 _ 。

リーダーマクロなどがある所為か、Common Lisp の界隈では特にその傾向が強いように思います。まぁ、 Scheme も SRFI でインデントベースの表記法を規定しているので結局皆考える事は一緒なのでしょうが…

S 式の表示を視覚的に工夫する試みも、学生の研究から Emacs での 「S 式を強調する 5 つの方法 : Five Approaches to S-Expression Highlighting 」までいろいろあります。

CGOL から数十年が立ち、多くの試みがなされましたが、 Lisp 使いは結局今も S 式をつかっています。なぜでしょうか? 曰く「優れた Lisp 使いには括弧が気にならなくなる」「インデントで構造は読むので」などなど諸説あります。 中には「Lisp 脳になると括弧が気持ち良くなる」といった胡散臭いもまである始末。 個人的な感想からいうと、Lisper は S 式の利点 を理解して 便利だから 使っているのであって、 もしこれよりも編集しやすく読みやすく、表現力に富む記法が開発されれば喜んで S 式を捨てるでしょう。

Lisp を勉強するほぼすべての人は、S 式の代替となる表現を考えようと試みます。 そして、ある程度の経験を積めばそれは実現できます。 しかし、それは S 式の利点 を 100% 引き継げない場合がほとんどなのです。

しかし、自分の体験に照し合わせてみると、この S 式の利点 は文章を読んだだけえは実感できませんでした。 自分の経験を思い出してみれば、最初は師匠の操作を後からのぞいてその編集操作のカーソルの奇妙な動きを観察してたものです。 現在は昔に比べれば圧倒的に Web で簡単に Lisp の情報は手に入りますが、この「S 式の便利さ」の編集部分を目にする機会は 依然として少ないように思います。 というわけで、マクロやプログラム==データなどいろいろ小難しく聞こえる S 式のメリットは よく見かけますが、「編集が楽」に関しては「Emacs を使えば楽」という程度の説明で あっさり終ってしまう場合が多いように思います。そこで、今回は括弧の編集における利点を解説してみようと思います。

S 式熟練レベル (Emacs 風エディタ)

対象外: 括弧嫌い (ここを読んでも時間も無駄です :-p )

  1. 括弧は悪だと信じている
  2. S 式を貶すことが目的の人々

レベル1 : 初心者

  1. 対応括弧ハイライトや S 式編集支援機能の存在を知らない
  2. 大量の括弧に尻込みする
  3. 開き括弧を入れたら、必ず対応する括弧を入力する
  4. 結局対応する括弧をがんばって数えて精神を消耗する
  5. 大量の括弧を恐れる
  6. 対応する括弧は同じレベルになければならないと思う
      (let 
           (
              (x
                   (
                      10
                   )
              )
           )
           ...
      )
    

レベル2 : 初級者

  1. Emacs/Vim の括弧ハイライト機能や支援機能の存在を知る
  2. 開き括弧を入れたら、まず対応する括弧を入力しないと落ちつかない
  3. 対応括弧の個数をハイライト機能を駆使して数える事を学ぶ
  4. プログラム==データ、S 式が〜という事を聞いた事があるが、意味はよくわからない
  5. 大量の括弧に不安を感じる
  6. 編集は行単位で、括弧単位の移動が面倒
      (let (
            (x 10)
            (y 10)
           )
         ...
      )
    

レベル3 : ユーザー

  1. 自動インデントの結果で、括弧のレベルがわかる事に気がつく
  2. インデントが合うようにハイライトを使いながら括弧を入力して揃える
  3. 初期入力時に閉じ括弧を対応させる意味があまりない事に気がつく
  4. 括弧を入力するたびに対応を数える必要がない事に気がつく / 対応を数える事を忘れている
  5. S 式単位の編集ができるようになるが、時々思いどおりにいかない
  6. 括弧はインデントが揃ってればいいと思うようになる
      (deufn foo ()
      __...              ; defun の次は空白 2
        (let ((x 10))
      ____...            ; let の中なので +2 して空白 4
      ____(let ((y 100))
      ______....         ; さらに let なので +2 して空白 6
      ______....
      ______....))
      __..               ; おや、インデントが 2 なので let は二つとも抜けて
      __..               ; defun 内のレベル だな、と括弧を数えずに対応が理解できる
        (let ((x 10)
              (y 10))
      ____...
      ____...)))
    

レベル4 : ベテラン

  1. Emacs の S 式移動コマンドを覚えはじめる
  2. 括弧の色を薄くしたりする / 括弧が意識から薄れて邪魔に見えなくなる
  3. 思いどうりに S 式単位の編集を使える。transpose-sexps をちょくちょく使う。
  4. 括弧を好むようになる

レベル5 : ハッカー

  1. Emacs の S 式移動コマンドをマスターする
  2. 括弧は空気のような存在。普段は存在を忘れているくらい無意識に操作できる。
  3. 括弧を愛するようになる

こんなとこでしょうか。

初心者や初級者が、ユーザーやベテランに進むために必要な S 式単位編集機能の説明です。

S 式の編集における Emacs 標準の機能 (http://lispuser.net/emacs/lisphacking.html#emacs ) の例を順番に示してゆきます。

関数単位の操作

Lisp における主要な単位である関数単位での操作です。

  • C-M-a : beginning-of-defun : defun の先頭へ移動
  • C-M-e : end-of-defun : defun の末尾へ移動
  • C-M-h : mark-defun : defun 一個分マーク
  • C-x n d : narrow-to-defun : defun 単位でナローイング

利点は次のとおり。

  1. 関数の先頭へ移動したい
  2. C-M-a を繰り返す事により、関数定義を移動できる。関数定義を二つ分↑に移動したければ、C-M-a C-M-a
  3. 関数定義 2 つ分↓に移動したければ、 C-M-b C-M-b
  4. 関数をコピペしたい : C-M-h M-w (mark-defun kill-ring-save)
  5. 関数をキルしたい : C-M-h C-w (mark-defun kill-regin)
  6. 今見ている関数以外は気にしない: C-x n d

リスト単位の操作

Lisp のソース、データを表現するリスト単位での操作です。

  • C-M-f : forward-sexp
  • C-M-b : backward-sexp

まずは、基本。同じレベルの要素を移動するための関数です。

;; -!- ポイント位置
(-!-a b c d) ;; 初期状態 a の上にカーソルが重なっている状態
(a-!- b c d) ;; C-M-f
(a b-!- c d) ;; C-M-f
(a b c-!- d) ;; C-M-f
(a b -!-c d) ;; C-M-b
(a -!-b c d) ;; C-M-b
(-!-a b c d) ;; C-M-b

これは、 S 式のレベルが同じ要素を移動してゆきます。 木構造にすると、同じ親の子の中を移動してゆくイメージです。

;; -!- ポイント位置
(-!-a (b (c) d) (e f)) ;; 初期状態 a の上にカーソルが重なっている状態
(a-!- (b (c) d) (e f)) ;; C-M-f 
(a (b (c) d)-!- (e f)) ;; C-M-f
(a -!-(b (c) d) (e f)) ;; C-M-b
(-!-a (b (c) d) (e f)) ;; C-m-b
  • C-M-n : forward-list
  • C-M-p : backward-list

これは使い所によります。同じレベルの S 式要素間を移動しますが、 「リスト」単位に移動して、アトム(シンボル、文字列その他)を飛ばします。 移動するのは「リストの先頭」、「リストの末尾」

(list :size (length key) :key key :value -!-(get-value key env)) ;; 前の関数呼び出しに一発で移動したい
(list :size -!-(length key) :key key :value (get-value key env)) ;; C-M-p
(list :size (length key)-!- :key key :value (get-value key env)) ;; C-M-n
(list :size (length key) :key key :value (get-value key env)-!-) ;; C-M-n
(list :size (length key) :key key :value -!-(get-value key env)) ;; C-M-
  • C-M-d : down-list
  • C-M-u : backward-up-list

これらが、重要です。特に C-M-d を C-M-u を使ってない人は、ダマされたと思って使ってみましょう。

;; -!- ポイント位置
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (let ((pair (and env (assoc symbol env))))
    (if pair
        (setf -!-(cdr pair) value)
        (setf (gethash symbol *global-env*) value))
    value))

;; C-M-u
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (let ((pair (and env (assoc symbol env))))
    (if pair
        -!-(setf (cdr pair) value)
       (setf (gethash symbol *global-env*) value))
    value))

;; C-M-u
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (let ((pair (and env (assoc symbol env))))
    -!-(if pair
       (setf (cdr pair) value)
       (setf (gethash symbol *global-env*) value))
    value))

;; C-M-u
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  -!-(let ((pair (and env (assoc symbol env))))
    (if pair
(setf (cdr pair) value) (setf (gethash symbol global-env ) value)) value))

;; C-M-d
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (-!-let ((pair (and env (assoc symbol env))))
    (if pair
        (setf (cdr pair) value)
        (setf (gethash symbol *global-env*) value))
    value))

イメージは捕めたでしょうか?今迄でてきたものを組み合わせて移動してみましょう。 ここから (and-!- env (assoc ....)) という状態にしてみましょう。

;; スタート
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (let-!- ((pair (and env (assoc symbol env))))
    (if pair
        (setf (cdr pair) value)
        (setf (gethash symbol *global-env*) value))
    value))

;; C-M-d C-M-d C-M-f C-M-d C-M-f : 5 ストローク
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (let ((pair (and-!- env (assoc symbol env))))
    (if pair
        (setf (cdr pair) value)
        (setf (gethash symbol *global-env*) value))
    value))

;; C-f C-f C-f C-f C-f C-f C-f C-f C-f C-f : 9 ストローク
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (lpet ((pair (and-!- env (assoc symbol env))))
    (if pair
       (setf (cdr pair) value)
       (setf (gethash symbol *global-env*) value))
    value))

さらに、この状態から *global-env* をマークしてみましょう。

;; 初期状態。ここから *global-env* をマークしたい
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (let ((pair (and-!- env (assoc symbol env))))
    (if pair
        (setf (cdr pair) value)
        (setf (gethash symbol *global-env*) value))
    value))

;; 1. C-M-u C-M-u C-M-u : 一旦 let の bindings リストを抜ける
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (let -!-((pair (and env (assoc symbol env))))
    (if pair
        (setf (cdr pair) value)
        (setf (gethash symbol *global-env*) value))
    value))

;; 2. C-M-f C-M-d で (if ...) リスト内に降りる : ここのがコツ
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (let ((pair (and env (assoc symbol env))))
    (-!-if pair
      (setf (cdr pair) value)
      (setf (gethash symbol *global-env*) value))
  value))

;; 3. C-M-f C-M-f C-M-f C-M-d で *global-env* を含む setf 文に移動
;;    (if 条件 then else) という構成なので、 if から else に移動するのは C-M-f x 3
;;    で、 else の setf 式に入りたいので C-M-d
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (let ((pair (and env (assoc symbol env))))
    (if pair
        (setf (cdr pair) value)
        (-!-setf (gethash symbol *global-env*) value))
    value))

;; 4. C-M-f C-M-d C-M-f C-M-f C-M-SPC
;;    もしは
;;    C-M-d C-M-f C-M-f C-M-SPC (動作の特性を利用した裏技)
(defun set-value (symbol value &optional env)
  "環境の値を書き換える"
  (let ((pair (and env (assoc symbol env))))
    (if pair
       (setf (cdr pair) value)
       (setf (gethash symbol-!- *global-env*) value))
    value))

このように、S 式移動コマンドを使うと 1. 〜 4. で 3 + 2 + 4 + 5 = 14 ストローク 普通に Emacs の編集で移動すると、 C-n C-n C-n M-f M-f ……… あれ? ふ、普通に Emacs の編集で移動すると、 C-n C-n C-n C-f C-f C-f C-f C-f C-f C-f C-f C-f C-f C-f C-f C-f C-f = 17 ストローク です。まぁ、単語移動は - のようなセパレータとして認識される文字がシンボル名に使える Lisp では 上手く動かないわけですが、そんなときに C-M-f や C-M-b を覚えているだけで移動の効率が UP します。

  • C-M-SPC or C-M-@ : mark-sexp
  • C-M-k : kill-sexp
  • C-M-q : indent-sexp

式単位マークしたりキルしたりインデントしたり。これは、 S 式操作と組み合わせると便利ですが、 直接マウスでポイント指定したときなんかも便利です。

;; height の計算式をコピペしたい
(-!-let* ((new-veclocity (+ (- velocity burn) 5))
     (fuel          (- fuel burn))
     (height        (- height (* (+ velocity new-veclocity) 0.5)))
     (elapsed       (+ elapsed 1))
     (velocity      new-veclocity))
 ...)
↓
;; C-M-d C-M-n C-M-n C-M-SPC M-w
(let* ((new-veclocity (+ (- velocity burn) 5))
     (fuel          (- fuel burn))-!-
     (height        (- height (* (+ velocity new-veclocity) 0.5)))
     (elapsed       (+ elapsed 1))
     (velocity      new-veclocity))
 ...)
 
;; 初期位置: 最後の velocity の更新を削除したい
(-!-let* ((new-veclocity (+ (- velocity burn) 5))
     (fuel          (- fuel burn))
     (height        (- height (* (+ velocity new-veclocity) 0.5)))
     (elapsed       (+ elapsed 1))
     (velocity      new-veclocity))
 ...)
  ↓
;; C-M-d C-M-n C-M-n C-M-n C-M-n C-M-k
(let* ((new-veclocity (+ (- velocity burn) 5))
     (fuel          (- fuel burn))
     (height        (- height (* (+ velocity new-veclocity) 0.5)))
     (elapsed       (+ elapsed 1)))
 ...)

インデントの例。自動インデントがあるからあんまり使わないですね。defun 単位での整形 C-M-h C-M-q とかならたまに使うかも。

(let ...
 ... ;; let 内にカーソルがあるときは、 C-M-u で ``(let`` の部分まで移動して、 C-M-q でインデント
 ...)
  • C-M-t : transpose-sexps

これは、S 式単位の入れ替えを行います。うまく使うと便利です。

まず、普通のリストで。

;; 1. 要素の入れ替え
'(a b c-!- d) ;;初期ポイント位置
'(a b d c-!-) ;; C-M-t

;; 2. 要素の入れ替え -- b を末尾にもっていきたい
'(a b-!- c d) ;;初期ポイント位置
'(a c d b-!-) ;; C-u 2 C-M-t

if でも大活躍。

;; 1. if の then と else の入れ替え
'(-!-if (< a 0) (* -1 a) a) ;; 初期ポイント位置
'(if (< a 0) (* -1 a)-!- a) ;; C-M-f C-M-f C-M-f
'(if (< a 0) a (* -1 a)-!-) ;; then と else の入れ替え

;; 2. その復旧
'(if (< a 0) a (* -1 a)-!-) ;; 初期ポイント位置
'(if (< a 0) a -!-(* -1 a)) ;; C-M-b
'(if (< a 0) (* -1 a) a-!-)    ;; C-M-t

let* でも。

;; 初期ポイント位置
(-!-let* ((a 100)
       (b zzz)
       (zzz (sin a))
       (x (+ a b)))
  ...)

;; C-M-d C-M-n C-M-n
(let* ((a 100)
       (b zzz)-!-
       (zzz (sin a))
       (x (+ a b)))
  ...)

;; C-M-t
(let* ((a 100)
       (zzz (sin a))
       (b zzz)-!-
       (x (+ a b)))
  ...)

cond でも。

;; 初期ポイント位置: (<= life 0) の条件を先頭にしたい!!
(-!-cond ((< life 100) (danger))
      ((> life 300) (powerup))
      ((<= life 0)  (chicken)))

;; C-M-n C-M-n C-M-n
(cond ((< life 100) (danger))
      ((> life 300) (powerup))
      ((<= life 0)  (chicken))-!-)

;; C-u -2 C-M-t
(cond ((<= life 0)  (chicken))-!-
      ((< life 100) (danger))
      ((> life 300) (powerup)))

で、このシンプルな操作の組み合わせで編集できるのが S 式の利点なわけです。 さらに、 Lisp のマクロによる構文拡張は結局 S 式ですので、このような編集 機能は Lisp が S 式を使って拡張される限り使いまわせるわけです。

今回紹介したような機能は Common Lisp に限らず、 Emacs Lisp や Scheme な ど S 式全般に使えます。眠くなってきたのでこのへんで。編集以外の括弧の理由については、 Lisp の解説書である ANSI Common Lisp や Practical Common Lisp がわかりやすいと思います。 では皆さん Happy Hacking !!!

posted: 2006/12/20 20:36 | permanent link to this entry | Tags: EMACS

(top)  (memo)  (rss)