Help us understand the problem. What is going on with this article?

'`'と','

More than 5 years have passed since last update.

lisp advent calendar

四日目の記事らしいです。
他のカレンダー参加者のレベルが高すぎて困りました。
以下について、利用の方言はCommon Lispです。
処理系は多分cltl1以降に準拠なら大丈夫だと思います。

Common Lispの難読記号

普段lispを書いているとlispは使う記号の種類が他の言語よりも少なくルールも少なく、演算子の結合則とか、使わなければいけない括弧のバリエーションも少なくて、なんてとっつきの良い言語なのだろうかと思っています。
意味不明な'$'とかどう結合するのか良くわからない'.'とかセパレータとしてはスペースがあれば事足りるはずなのに一々書かなくてはならない','とかと縁なくて良かったなーなどと思って日々の生活はしているのですが、マクロを書く時の簡易の為の'`'と','が、マクロを書く時に考える事の特殊性とあいまって、他所からみて慣れないと混乱する記号の利用なのかな…と思ったので難読っぽいマクロにを題材に少しだけ。

単純なバッククォートの利用

バッククォート('`')は式中で評価される場所と評価されない場所を決めるのに使います。評価させる式にはカンマ(',')を付け、評価させない式は何もつけません

`(+ 1 2 ,(+ 3 4))
;; ->(+ 1 2 7)

カンマのバリエーションに',@'というものもあります。これは、','と動作は似ていますが、',@'直後のリストを外側のリストの要素として展開する部分が異なります。

`(+ 1 2 ,(list 3 4))
;;->(+ 1 2 (3 4))

`(+ 1 2 ,@(list 3 4))
;;-> (+ 1 2 3 4)

','に比べて括弧がひとつ取れます。

バッククォートのネスト

バッククォート一つの際はともかくネストしたものは少し複雑です。

;;cltl2 appendix C をつまんだもの。
(let ((q '(r s)))
  ``(q
     ,q
     ,,q
     ,@q
     ,,'q
     ,',q
     ',,q
     ,@,q
     ,,@q
     ,@,@q))
;;-> `(Q ,Q ,(R S) ,@Q ,Q (R S) ',(R S) ,@(R S) ,R ,S ,@R ,@S)

私は順に以下のように覚えています。

  1. カンマ無しは展開しない、
  2. カンマ一個は後で展開(展開形の中で展開されるように)する、
  3. カンマ二個は展開して後でも展開する。
  4. 後で展開するspliceもする
  5. カンマ二個ただし'qの評価
  6. カンマ二個なので評価する、左の,と'は打ち消しあう
  7. カンマ二個に'がついている。
  8. 展開した結果をsplice
  9. 展開spliceした結果を展開
  10. 展開spliceした結果を展開splice

マクロって…

バッククォートの利用はマクロに限られているわけではありませんが、
一番使いでがあるのがマクロの記述の時です。
Common Lispのマクロは単純に評価されるべき式を返す関数として定義します。
最も単純な例はifからwhenやunlessを作るものでしょうか

(defmacro when (test &body body)
  `(if ,test (progn ,@body)))

(defmacro unless (test-not &body body)
  `(if ,test-not nil (progn ,@body)))

こういうマクロを書いていく際に意識しているのは、
「引数をどこに埋めれば良いんだっけ?」というくらいの事です。

もう一歩先へ

ネストしたバッククォートを含む歯応えのあるマクロもみてみましょう。
Peter Seibel 'Praitical Common Lisp'でも出てくる(同種の物はAlexandriaにも収録Quickutilから利用可能)、once-onlyです。

(defmacro once-only (names &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    ;; bind in user-macro
    `(let (,@(loop for g in gensyms collect `(,g (gensym))))
       ;; bind in final expansion
       `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
          ;; bind in user-macro
          ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
             ,@body)))))

ぱっと見の破壊力は増しました。
これが簡単にわかる人はここでおしまい。

割と脅しに使われるこのマクロも別に難しいってほどでも無いはず…
なので読んで(考えて)みます。

once-onlyのつかいみち

望まれない変数補足を避ける際に使用します。

(defmacro 2*-once-only (x)
  (once-only (x)
    `(+ ,x ,x)))

(defmacro 2* (x)
  `(+ ,x ,x))

(list (let ((i 1))
        (list (2*-once-only (incf i)) i))
      (let ((i 1))
        (list (2* (incf i)) i)))
;; => ((4 2) (5 3))

(incf i)を2*-once-onlyは一回2*では二回評価されることで
マクロの評価結果が変わってしまいます。

once-onlyは何に展開されるのか。

once-onlyの目的はマクロに渡されるシンボルの評価を一回だけし、結果の値にgensymされたシンボルを束縛する'式'を生成することです。これが具体的に何なのかは展開形をmacroexpand-1で生成してみれば判ります。

(macroexpand-1 '(once-only (x) x))

;;=> (LET ((#:G1201 (GENSYM))) ;;gensymされたsymbol #:g1201をgensymの結果に束縛
;;    `(LET ((,#:G1201 ,X)) ;;指定されたxが評価され結果を束縛
;;       ,(LET ((X #:G1201)) ;;束縛された結果の値は以下xで参照可能
;;          X))) ;;以下body

;;シンボルが二つの場合の結果もみてみる。
(macroexpand-1 '(once-only (x y) x))

;;=>(LET ((#:G1202 (GENSYM)) (#:G1203 (GENSYM)))
;;  `(LET ((,#:G1202 ,X) (,#:G1203 ,Y))
;;     ,(LET ((X #:G1202) (Y #:G1203))
;;        X)))

展開形をみると、マクロを普段から書いている人が変数補足を
避けるためのイディオムが展開されていることがわかります。

複雑…書けるの?

lispの式が脳内でevalできて、上記イディオムがちゃんと理解できるなら、
展開した結果を生成できる式を書くだけです。
逆に変数補足を防ぐイディオムが何故必要なのか?という人にこのマクロを書けというのは無理な話です。
ドメインの知識が無いのですから。

「何故」を理解してさえいれば、ちょっと複雑なバッククォートをなんとかするだけです。
「マクロを書く」と言う際には常にこの展開した型のゴールを最初に作っておいて、
(今回は既存のマクロを展開してみただけですが)
書きかけのマクロを展開して結果が望みどおりになっているかどうかを確認しながら、
書きたしていく作業になります。これくらいのものになると、一発で書けたりはあまりしないのではないかと思います。

マクロとバッククォートが複雑な印象になりがちなのは

  1. 展開形が脳内で定まっていない
  2. バッククォートについて何が生成されるのか良くわからない

という両方の症状を抱えたまま考えることが原因ではないかと思っています。

複雑だという意見や批判もありますが、
replがあるんだからやってみりゃよい、展開形をみてみればそのうちなんとかなるという場合も多いと思います。

おわり

想定読者がどの層だったっけか?(once-onlyが読めそで読めない人?)とか色々思う所(ハショリスギタ感)はありますが、
「マクロ別に難しくないよ!」ということを言おうとして「面倒くさそう」という印象付けに成功したような気がしてなりません。残念。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした