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

newLISP で On Lisp する...第8章

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

 第8章 いつマクロを使うべきか
 第7章 マクロ で Common Lisp 並みのマクロ を newLISP で書く手法を紹介しましたが、マクロを使うべき対象も同じでしょうか?

 他に何も関係しないとき にある1~7の項目のうち、7番目を除けば、全て、newLISPにも当てはまります。言い切っていますが、私の知る限りですけどね。5番と6番が変数捕捉の問題を起こすのも同じです。つまり気を付けなくてはいけないということ。7番目のは、コンパイル機能の無い newLISP には、無縁でしょう。したがって、newLISP でのマクロの利用法は、6通りと言いたいところですが、実はもう一つあります。前にも書いていますが、newLISP では、特殊なデータ構造を使わない限り、副作用を持つ関数を書けません。つまり、副作用を主目的とする場合も、マクロになります。

 マクロと関数どちらがよい? と言われれば、第7章(その2)の最後に書いた内容です。コンパイルの無い newLISP では、“On Lisp”本書のオペレータavg は、マクロではなく関数で書くべきでしょう。

(define (avg)
  (div (apply add (args)) (length (args))))

(define-macro (avgm)
  (letex (sum (cons 'add (args)))
    (div sum (length (args)))))

 動作は、試すまでもないですね。
 マクロの長所 として、newLISP で当てはまるのは、“On Lisp”本書の2番目だけ? 反対に、 マクロの短所 では、“On Lisp”本書の4項目全てが当てはまる。これは、どのプログラム言語も同じでしょうかね。

 ちっとも、実装例が出ませんが、マクロの応用例 から、関数move-objs を newLISP で実装してみます。
 この関数は、戻り値ではなく、副作用を目的とした関数です。つまり、関数型言語より、手続き型言語の向きということ。それでも、Lisp が向いているのは、マクロの長所 の2番目が当てはまるから。もちろん、newLISP のマクロにも当てはまります。しかし、前述のように newLISP では、副作用を目的とする関数を書けません。そこで、これまた前述の特殊なデータ構造を使います(笑)。
 前置きが長くなりましたが、元々の例から(defun は、newlisp-utility.lsp に、multiple-value-bind は、こちらに定義してあります)。

(defun move-objs (objs dx dy)
  (multiple-value-bind (x0 y0 x1 y1) (bounds objs)
    (let (len (length objs))
      (dotimes (i len)
          (inc (objs i obj-x) dx)
          (inc (objs i obj-y) dy))
      (multiple-value-bind (xa ya xb yb) (bounds objs)
        (redraw (min x0 xa) (min y0 ya)
                (max x1 xb) (max y1 yb))))))

 dolist の代わりに dotimes を使い、引数objs の書き換える部分の特定にインデックス機能を使っています。他は、本書の例とほぼ同じです。どこにも、特殊なデータ構造なんて無いですね。それは、引数として与えるデータにあります(values は、こちらに定義してあります)。

(set 'square:square '((0 0 1 0)(1 0 0 1)(1 1 -1 0)(0 1 0 -1)))
(define obj-x  0)
(define obj-y  1)
(define obj-dx 2)
(define obj-dy 3)
(defun bounds (objs)
  (let (x-objs (append (map first objs)
                       (map (fn (x) (+ (x 0) (x 2))) objs))
        y-objs (append (map second objs)
                       (map (fn (x) (+ (x 1) (x 3))) objs)))
    (values (apply min x-objs)
            (apply min y-objs)
            (apply max x-objs)
            (apply max y-objs))))
(defun redraw (x0 y0 x1 y1)
  (println "(" x0 "," y0 ")-(" x1 "," y1 ")") t)

 見ての通り、特殊なデータ構造とは、context の default functor です。ついでに、未定義のシンボルと関数を定義しています。
 動作はというと

> square:square
((0 0 1 0) (1 0 0 1) (1 1 -1 0) (0 1 0 -1))
> (move-objs square 10 10)
(0,0)-(11,11)
true
> square:square
((10 10 1 0) (11 10 0 1) (11 11 -1 0) (10 11 0 -1))

 見ての通り、オペレータmove-objs は関数にもかかわらず、副作用をもたらします。
 リストデータで与えた場合は、

> (set 'square2 '((0 0 1 0)(1 0 0 1)(1 1 -1 0)(0 1 0 -1)))
((0 0 1 0) (1 0 0 1) (1 1 -1 0) (0 1 0 -1))
> (move-objs square2 10 10)
(0,0)-(11,11)
true
> square2
((0 0 1 0) (1 0 0 1) (1 1 -1 0) (0 1 0 -1))

 と、副作用を生じません。
 さて、マクロwith-redraw を用意すると次のようになります。

(define-macro (with-redraw)
; (with-redraw (var objs) &body body)
  (letex (_objs (args 0 1)
          _body (append '(dotimes)
                        (list (cons (args 0 0) 'len))
                        (1 (args))))
  (multiple-value-bind (x0 y0 x1 y1) (bounds _objs)
    (let (len (length _objs))
      _body)
      (multiple-value-bind (xa ya xb yb) (bounds _objs)
        (redraw (min x0 xa) (min y0 ya)
                (max x1 xb) (max y1 yb)))))) 

(defun move-objs (objs dx dy)
  (with-redraw (i objs)
    (inc (objs i obj-x) dx)
    (inc (objs i obj-y) dy)))

 マクロに、gensym を使っていないのは、前回紹介した方法で変数捕捉問題を回避することを前提にしているからです。gensym は、すでに用意してあるので、使ってもよいのですが、その場合は、letex 内で定義します。この章のメインは、変数捕捉ではないので(汗)。ポイントは、マクロwith-redraw が用意されたことで、図形の move の記述が簡単になる。そして、rotate や reverse や transpose なんかも簡単な記述を期待できるということです。つまり、本書にある、マクロの主要な利用法の一つ、埋め込み言語の実装 が可能だということ。

 さて、第8章 いつマクロを使うべきか のまとめです。

  • newLISP でのマクロの利用法は、Common Lispとほぼ同じです。ただし、コンパイルに関する利点はありません。

 以上、如何でしょうか?

広告