Archive for 2010年7月6日|Daily archive page

newLISP で On Lisp する...第7章(その1)

(この blog は、“short short story または 晴耕雨読な日々”からの引越してきたもの。スクリプトは、newLISP V10.2.1 以降で動作するように書き直しています。
defun 等の newLISP組込関数に無い関数は、特に断らない限り、newlisp-utility.lsp に定義してあります。)

 第7章 マクロ 。“On Lisp”本書によれば、ここから第10章までが、マクロの基礎講座。
 最初は、マクロはどのように動作するか です。 newLISP のマクロは、CommonLisp のマクロと異なりますが、まずは、同じ点から

  • 引数は、評価されずに展開される。

 newLISP と Common Lisp 共に、これが、関数とマクロの最大の違いでしょう。
 そして、newLISP と CommonLisp のマクロの違いです。

  • CommonLisp のマクロは、マクロ本体で評価したもの(通常は式)を返し、マクロが呼び出された場所で、再び評価される。(つまり、マクロ内とマクロ外とで、評価が2回ある。)
  • newLISP のマクロは、マクロ内で評価されたものを返す。

 ここで言う Common Lisp のマクロ内の評価は、引数として与えられた(評価されていない)式を変形するという意味です。マクロ外の評価は、変形された式を文字通り評価することです。私は、この違いを認識するまで、Common Lisp のマクロの理解にてこずりました
 そして、newLISP には、Common Lisp でいうの2回目の評価が無いということ。この違いを例で見てみましょう。

(define-macro (nil! var)
  (list 'setq var nil))

 これは、“On Lisp”本書の例をそのまま、newLISPに当てはめたものです。
 動作は、

> (setq x 1)
1
> x
1
> (nil! x)
(setq x nil)
> x
1
> (eval (nil! x))
nil

 となります。newLISP では、全ての変数が、初期値に nil を割り当てられ、設定されたのかどうかわからないので、動作を確認する前に、変数に初期値を入れています。
 さて、結果は、マクロnil! の本体で評価された結果出来た式が現れています。Common Lisp であれば、最初の例では、この式が評価されて、変数 x に nil が入りますが、newLISP では、式のままです。マクロで展開された式に eval を使って評価すると、変数 x に nil が入ります。
 ということで、本書のマクロ例を newLISP で書くと、次のようになります。

(define-macro (nil! var)
  (set var nil))

 動作は、

> (setq x 2)
2
> x
2
> (nil! x)
nil
> x
nil

 という具合に、所望の動作をします。
 Common Lisp 風に setq を使うなら、

(define-macro (nil! var)
  (setq (eval var) nil))

 といったところ。newLISP のマクロでは、文字通り、引数が本体に、評価されずに渡されます。つまり、引数は、クォートされて本体に渡されるのです。そのため、setq を使ったマクロnil! では、eval が必要だったのです。
 こんな、newLISP のマクロですが、Commno Lisp並みのマクロ表現ができるのか? 結論から言えば、出来ます(たぶん、笑)。その回答が、次の項になります。

 今回のメイン、バック・クォート です。 Common Lisp では、マクロが呼び出された場所で評価される式を作るために、マクロを組みます。そして、式を作るのに、組込の list と同じ働きをする `(バック・クォート)を多用します。その際、 ` 内で使われる、 ,(カンマ)と ,@(カンマ・アット) との組み合わせは、必須みたいなものでしょう。
 newLISP には、バック・クォートがありません。記述した式がそのまま、展開されます。そして、 ` 内で使われる ,(カンマ)と ,@(カンマ・アット)に取って代わるのが、newLISP組込の letex です。“On Lisp”本書では、バック・クォートを使う・使わない例が記述されていますが、newLISP では、letex を使う例だけ示します。

(define-macro (nif expr pos zero neg)
  (letex (_expr expr
          _pos  pos
          _zero zero
          _neg  neg)
  (case (sgn _expr)
    (1 _pos)
    (0 _zero)
    (-1 _neg))))

 引数名を明示する書き方と、そうでない方です。動作は、本書にある CommonLisp でバック・クォートを使う・使わない例の動作と全く一緒です。

> (map (fn (x) (nif x 'p 'z 'n)) '(0 2.5 -8))
(z p n)

 さて、letex の役割は、let と同じように見えますが、違いを前述の nil! で見てみましょう。次の四つは全て同じ動作です。

(define-macro (nil! var)
  (letex (_var var)
    (setq _var nil)))

(define-macro (nil! var)
  (let (_var var)
    (setq (eval _var) nil)))

(define-macro (nil! var)
  (letex (_var var)
    (set '_var nil)))

(define-macro (nil! var)
  (let (_var var)
    (set _var nil)))

 如何でしょうか? let では、引数がそのまま評価されずに本体の展開式に渡されますが、letex では、クォートが外されて、本体に渡されています。つまり、letex 内で引数が評価されたわけです。

(define-macro (test var)
  (let (_var var)
    (letex (_var2 var)
  (list _var _var2))))

 というマクロを定義して、

> (setq x 1)
1
> (test x)
(x 1)
> (list 'x x)
(x 1)

 newLISP では、マクロが呼び出された場所での(2回目の)評価がありません。そこで必要となるのが、letex なのです。letex には、引数が評価されずに(クォート付きで)わたされますので、それを評価して、変数に入れます。上記例では、変形してませんが、ここで変形もできます。定義された変数は、letex に続く本体の中に展開され、評価されます。この評価の際、変数は変形された式となっているわけですから、Common Lisp の2回目の評価に相当する評価になるわけです。ですから、newLISPでのマクロには、letex が、ほぼ必須といってよいでしょう。もちろん、使わなくても、定義できます、例えば、defun の定義とか。
 次に、マクロour-when を例に、CommonLispの `(バック・クォート)、 ,(カンマ)と ,@(カンマ・アット)と newLISP の letex の関係を見ていきましょう。

(define-macro (our-when test)
  (letex (_test test
          _body (cons 'begin (args)))
    (if _test
        _body)))

 Common Lisp で ,test となるところは、そのまま、letex で _test に設定しています。問題は、,@(カンマ・アット)の部分です。本書の CommonLisp の例で、body となっている部分と (args) は、同じ内容です。CommonLisp の ,@(カンマ・アット)は、リストの要素を展開してくれます。しかし、newLISP には、それに相当するものがありません。そこで、,@(カンマ・アット)を適用したい場所を括っている括弧までを letex で設定します。それが、3行目です。 begin は、CommonLisp の progn に相当する newLISP の組込です。 begin(args)cons することで、(progn ,@body) に相当する部分を用意しています。 Common Lisp で newLISP風に書くとこうなります。

(defmacro our-when (test &body body)
  (let ((_test test)
        (_body (cons 'progn body)))
    `(if ,_test
         ,_body)))

 Common Lispのマクロをこの状態に変形して、let を letex に変え、バック・クォートとカンマを削除すれば、newLISP用マクロの完成です(笑)。
 もちろん、letex は、関数でも使えます。第5章で散々使いましたので、例は省略します。

 次の 単純なマクロの定義 からは、our-while の定義だけで十分でしょう。

(define-macro (our-while)
  (letex (_test (args 0)
          _body (cons 'begin (1 (args))))
    (do ()
        ((not _test))
          _body)))

 引数の test は _test に再定義しているだけですから、(args) から直接取っています。_body は、begincons することで、本書の例で ,@body が展開するものと同等になるようにしています。newLISPには、do はありません。第3章(その1)で使ったマクロです(使用には、newlisp-utility.lsp が必要)。マクロの展開にマクロを使っても動作します。

> (let (i 0) (our-while (< i 2) (print i) (++ i)) i)
012

 マクロ展開の確認 からは、次回に。

 以上、如何でしょうか?

広告