データ構造の抽象化 - 構造体

構造体

構造体(ストラクチャ)とは、複数の値をひとかたまりとしてグループ単位で扱うデータ構造です。例えば、一人の人間を表す時に氏名・生年月日・性別をセットで扱う必要が出てきます。連想リストは柔軟性がありますが、すこし面倒です。あるいは別の方法として、リストに格納して一番目のデータは氏名、三番目のデータは性別を表すと決めてプログラムを書いていっても良いのですが、それらを統一的なインターフェースで扱うことが構造体というアイデアになります。

Common LISP は構造体を最初から使用できます。AutoLISP でも次の関数を用意することによって、同様のことができるようになります。

(defun defstruct (structure arglist / defstruct:field-r argNameList structure-string)
  (defun defstruct:field-r (flist)
    (if flist
      (progn (set (read (strcat structure-string "-" (vl-symbol-name (car flist))))
                  (eval (list 'lambda
                              '(alist)
                              (list 'nth (1+ (vl-position (car flist) argNameList)) 'alist)
                        )
                  )
             )
             (defstruct:field-r (cdr flist))
      )
    )
  )
  (setq structure-string (vl-symbol-name structure)
        argNameList      (mapcar 'car arglist)
  )
  ;;
  (set                                  ; function MAKE-<STRUCTURE>
    (read (strcat "make-" structure-string))
    (eval
      (list
        'lambda
        argNameList
        (cons
          'list
          (cons
            (list 'quote structure)
            (mapcar (function
                      (lambda (arg)
                        (list 'if (car arg) (car arg) (list 'eval (list 'quote (cdr arg))))
                      )
                    )
                    arglist
            )
          )
        )
      )
    )
  )
  ;;
  (set (read (strcat structure-string "-p")) ; function <STRUCTURE>-p
       (eval (list 'lambda '(alist) (list '= '(car alist) (list 'quote structure))))
  )
  ;;
  (defstruct:field-r argNameList)       ; function <STRUCTURE>-<FIELD ... >
  structure
)

defstruct 関数のコードの説明は省略して、この関数の使い方を通して構造体の説明をしていきます。

次のように実行すると、x と y というフィールドをもつ point という名前の構造体が定義されます。第一引数が構造体名、そして第二引数が構造体がもつフィールド x,y についてを連想リストの形式で渡しています。フィールド名とペアを組んでいる値は、nil により初期値が与えられなかった場合のフィールドの既定値を表し、それは「評価」された上でフィールドにセットされます。

_$ (defstruct 'point '((x . 0.0) (y . 0.0)))⏎
POINT

ここで定義されるというのは、次のような関数が新たに作成されるということです。

関数 説明
(make-point x y) フィールド x,y の値を持つ point 構造体を作成します。
(point-p item) item が point 構造体であるか判定します。
(point-x pointStructure) pointStructure のフィールド x の値を返します。
(point-y pointStructure) pointStructure のフィールド y の値を返します。

point 構造体を実際に作成してみます。やってみれば判るとおり、構造体と仰々しく言いながらも実際はリストで表現されています。LISP は型チェックがなく、また自動で生成した関数は汎用的なものですので、三番目の例のように目的と違った値を含んだ構造体も作成することができます。これについては、最後に改良の方法を述べます。

_$ (setq p1 (make-point 1.0 2.0))⏎
(POINT 1.0 2.0)
_$ (setq p2 (make-point 1.0 nil))⏎
(POINT 1.0 0.0) _$ (setq p3 (make-point "Auto" "CAD"))⏎
(POINT "Auto" "CAD")

その他の関数の使用例は次の通りです。

_$ (point-p p1)⏎
T
_$ (point-x p1)⏎
1.0
_$ (point-y p1)⏎
2.0

これらの関数で何が実現できたかというと、リストで表されているひとかたまりのデータの具体的な仕様を知る必要が無く、関数を通してデータのグループを作成したり値を取り出したりすることができるようになったということです。つまり、ここではデータ構造が抽象化されています。そして、新しくフィールドが追加されるような仕様変更があったとしても、プログラムの変更は最小限に抑えられます。

フィールドにリストを保持するタイプの構造体を定義してみます。既定値とする値は「評価」された上でセットされるので、すこし回りくどいですが次のように defstruct 関数の引数を渡します。

_$ (defstruct 'LineSegment '((start quote (0.0 10.0)) (end quote (20.0 30.0))))⏎
LINESEGMENT

実際に構造体を作ってみます。

_$ (make-LineSegment nil nil)⏎
(LINESEGMENT (0.0 10.0) (20.0 30.0)) _$ (make-LineSegment '(100.0 110.0) nil)⏎
(LINESEGMENT (100.0 110.0) (20.0 30.0))
_$ (make-LineSegment '(100.0 110.0) '(120.0 130.0))⏎
(LINESEGMENT (100.0 110.0) (120.0 130.0))

既定値としている値をわざわざ「評価」するようにしているので、次のように式を用いて既定値を指定することができます。segment フィールドに (make-LineSegment '(0.0 0.0) '(0.0 0.0)) を評価して LineSegment 構造体を格納しています。

_$ (defstruct 'Leader '((segment make-LineSegment '(0.0 0.0) '(0.0 0.0)) (text . "A") (alignment . 'left)))⏎
LEADER
_$ (make-Leader nil nil nil)⏎
(LEADER (LINESEGMENT (0.0 0.0) (0.0 0.0)) "A" LEFT) _$ (make-Leader (make-LineSegment '(10.0 20.0) '(30.0 40.0)) "LeaderText" 'RIGTH)⏎
(LEADER (LINESEGMENT (10.0 20.0) (30.0 40.0)) "LeaderText" RIGTH)

有理数の四則演算

構造体の使用例として、有理数、いわゆる分数の四則演算を行う関数を作ってみます。

(defstruct 'rational '((numerator . 0) (denominator . 1)))

(defun rational:add (x y)
  (make-rational
    (+ (* (rational-numerator x) (rational-denominator y))
       (* (rational-numerator y) (rational-denominator x))
    )
    (* (rational-denominator x) (rational-denominator y))
  )
)

(defun rational:sub (x y)
  (make-rational
    (- (* (rational-numerator x) (rational-denominator y))
       (* (rational-numerator y) (rational-denominator x))
    )
    (* (rational-denominator x) (rational-denominator y))
  )
)

(defun rational:mul (x y)
  (make-rational
    (* (rational-numerator x) (rational-numerator y))
    (* (rational-denominator x) (rational-denominator y))
  )
)

(defun rational:div (x y)
  (make-rational
    (* (rational-numerator x) (rational-denominator y))
    (* (rational-denominator x) (rational-numerator y))
  )
)

最初にフィールド numerator(分子)denominator(分母)を持つ rational 構造体が定義されています。つまりは先ほどの例と構造が同じですが、次のような関数が自動的に作成されました。

関数 説明
(make-rational numerator denominator) フィールド numerator, denominator の値を持つ rational 構造体を作成します。
(rational-p item) item が rational 構造体であるか判定します。
(rational-numerator rationalStructure) rationalStructure のフィールド numerator の値を返します。
(rational-denominator rationalStructure) rationalStructure のフィールド denominator の値を返します。

以降の四則演算の関数内では、自動的に作成された関数を使用することによって rational 構造体が実際には具体的にどのような構造をもっているかが隠されています。

計算例は以下の通りです。

_$ (setq a (make-rational 2 5))⏎
(RATIONAL 2 5)
_$ (setq b (make-rational 4 5))⏎
(RATIONAL 4 5)
_$ (rational:add a b)⏎
(RATIONAL 30 25)
_$ (rational:sub a b)⏎
(RATIONAL -10 25)
_$ (rational:mul a b)⏎
(RATIONAL 8 25)
_$ (rational:div a b)⏎
(RATIONAL 10 20)

上の例は、計算は合っているようですが約分されていません。これを含めて、make-rational 関数の次の点を改良します。

  1. 約分を行う
  2. フィールドは自然数(整数)とする
  3. 分母が負の時は、分子の方でその分数の正負が判るようにする

defstruct 関数で自動的に作成された make-rational 関数を生かしながら、関数を拡張して make-rational 関数を再定義します。具体的には (eval make-rational) が自動で作成された関数オブジェクトを表すことになり、その関数オブジェクトを含んだ新しい関数をあらためてシンボル make-rational に代入します。

(setq make-rational
       (eval (list 'lambda
                   '(numerator denominator / g)
                   (list 'if
                         '(zerop numerator)
                         (list (eval make-rational) '0 '1)
                         (list 'progn
                               '(setq g (gcd (abs numerator) (abs denominator)))
                               (list 'if
                                     '(minusp denominator)
                                     (list (eval make-rational)
                                           '(- (/ (fix numerator) g))
                                           '(- (/ (fix denominator) g))
                                     )
                                     (list (eval make-rational)
                                           '(/ (fix numerator) g)
                                           '(/ (fix denominator) g)
                                     )
                               )
                         )
                   )
             )
       )
)

新しい make-rational 関数の元での実行例は次の通りです。

_$ (setq a (make-rational 2 5))⏎
(RATIONAL 2 5)
_$ (setq b (make-rational 4 5))⏎
(RATIONAL 4 5)
_$ (rational:add a b)⏎
(RATIONAL 6 5)
_$ (rational:sub a b)⏎
(RATIONAL -2 5)
_$ (rational:mul a b)⏎
(RATIONAL 8 25)
_$ (rational:div a b)⏎
(RATIONAL 1 2)

Common LISP には最初からありましたが、AutoLISP には構造体も有理数(分数)の四則演算も有りませんでした。ところが少しだけ関数を書いてやることで、あたかも最初からサポートされていたかのように言語そのものを拡張することができました。このようなことができるのが、LISP の面白いところです。長い歴史の中で新しいプログラミング法のアイデアが出てきても、言語の根本の所を変えることなく新しいアイデアを柔軟に取り込むことができる LISP の強さを表しています。