2014-04-15-01-演算子オーバーロードについても考える - 2 - project-enigma

2014-04-15-01-演算子オーバーロードについても考える - 2

>> Site top >> weblog >> 月別アーカイブ >> 2014年04月のlog >> 2014-04-15-01-演算子オーバーロードについても考える - 2

最終更新日付:2014/04/15 23:50:00


演算子オーバーロードについても考える - 2

2014 年 04 月 15 日

昨日はなんだかもごもごした感じで終わってしまったが、それは書きながら別のことに気付いてうろたえていたからだ。今日は、ひとまずわかっていることを書いていく。

 

昨日書いた演算子オーバーロード的な話題は、基本的にはマクロの出番はない。というのも、演算子というのはその種類ごとにパラメータの数が決まっているからだ。比較系の演算子であれば2つだし、インクリメント/デクリメント演算子であれば1つだ。だからそういった演算子にあたる総称関数を直接的に defgeneric してあげれば良くて、マクロでくるんでやる必要はない。うん、基本的には。

しかし、代入(を伴う)演算子となるとそうはいかない。つまり、C++ で言えば operator=() とか operator+=() だ。これは Common Lisp で setf や incf が関数で実装できないのと同じ理由だ。最初のパラメータが指す「場所」の値を書き換えなければならない。つまりマクロが必要になるということだ。

では、昨日の冒頭あたりで書いた、container-assign などはどうやって実装されているのだろうか。これは簡単で、渡された(C++ 風に言えばポインタが指している先の)コンテナの「中身」を書き換えているだけだ。だからこそそれは関数(実際には defmethod)で書くことができた。

しかし、今やろうとしていることは違う。それが問題だ。コンテナも、反復子も、その他についても、CL-STL の世界での「代入」を標準化しようとしている。だから、何かが置かれているどこかに対して代入を行うとき、代入先に元々あったものが fixnum だろうと make-instance された何かであろうと、統一的に動いて欲しいわけだ。たとえば、以下のような状況では単純に a に 20 がセットされて欲しい。

(let ((a 10)
      (b 20))
  (opr= a b)
  (values a b))

; => 20
;    20

 

しかし、opr= 呼び出しが2つのコンテナオブジェクトだった場合、別の動きをして欲しいわけだ。つまり、以下のような場合、

(let ((a (stl:new :vector ()))
      (b (stl:new :vector (#{0 1 2 3 4}))))
  (opr= a b)
  (values a b))

 

単純に b が指している vector オブジェクトを a も指す(それに伴ってそれまで保持していた vector インスタンスを捨てる) ── つまり setf する ── のではなく、「b が指しているコンテナの中身を a の中身にコピーする」ということなのだ。

上手くいくかどうかわからないが、今考えているのは、総称関数呼び出しと setf に展開されるマクロを用いる方法だ。以下のようにしておけば、

(defgeneric operator= (a b))
(defmethod  operator= (a b)
  b)

(defmacro opr= (a b)
  `(setf ,a (operator= ,a ,b)))    ; 注意! 間違っている!

 

非効率ではあるが、デフォルトでは (opr= a b) は (setf a b) と同じことになる。コンテナなどの場合、operator= を特定化し、b の中身を a に設定してから a を返せば良いことになる。

上記のコードはイメージを示すことが目的なので、間違っていることを承知で簡単に書いた。Paul Graham も On Lisp で書いている通り、「setf が絡むマクロは作るのが難しい」。今回 On Lisp を読み直して、以下のようにすれば良い(らしい)ことがようやくわかった。

(defgeneric operator= (a b))
(defmethod  operator= (a b)
  b)

(defmacro opr= (a b)
  (multiple-value-bind (vars forms var set ref) (get-setf-expansion a)
    `(let* (,@(mapcar #'list vars forms))
       (let ((,@var (operator= ,ref ,b)))
         ,set
         ,@var))))

 

うん、おそらく非効率だ。しかし意図した通りに動作するように見える。展開形を見れば、副作用を伴うパラメータを渡しても問題ないことがわかる。

しかし、この仕組みを大々的に導入するかどうかについては、正直どうしたものか迷っている。というのも、fixnum のような値と、make-instance で作るようなオブジェクトの扱いが違うことを、Lisp プログラマは当然と思っているからだ(悪く言っているわけではないよ)。たとえば、以下のコードの結果と、

(let ((a (cons 0 1))
      (b (cons 2 3)))
  (setf a b)
  (setf (car b) 9)
  (values a b))

=> (9 . 3)
   (9 . 3)

 

以下のコードの結果は扱いが違うように(自分には)見える。上の例はポインタ的に振る舞うように見えるし、下の例は値的に振る舞うように見える。

(let ((a 10)
      (b 20))
  (setf a b)
  (setf b 9)
  (values a b))

=> 20
    9

 

Lisp を学び始めたころ、Lisp にはポインタという概念がない ── というか裏に隠れている ── という説明に感じた違和感はこれだった。上記の例を見れば、値的に振る舞うものもあることはわかる。いったい、どれが値的に振る舞い、どれがポインタ的に振る舞うんだ? 数値や make-instance するようなモノはまぁ容易に想像がつく(想像に過ぎないけど)。しかし、構造体は? 文字列は? きっと、仕様のどこかに明快に書いてあるのだろう。しかし自分の英語力では、隅から隅まで読むのには時間がかかり過ぎるのだ。だから自分はこの点に関する一般的な規則とでもいったものをまだ知らない。

話を戻そう。少なくとも、上記のような方法で「代入演算子」を標準化し、STL で言うところの operator= を実装させることはできそうだ。しかし、pair だの反復子だのコンテナだのといったあれこれの代入以外にも、無視できない重要な代入がある。前回書いた replace をもう一度見てみよう。

(defmethod replace ((first forward-iterator)
                    (last  forward-iterator)
                     old-val new-val &optional (eql-bf #'eql))
  (...))

 

これは、範囲 [first, last) 内の要素それぞれについて、それが old-val に「等しければ」 new-val に「置き換える」。この、「置き換える」という部分で代入が発生している。それは、前方向反復子が指している「場所」への代入である。そしてこのような場合、設定すべき場所にあるモノと、設定すべき値はまったく別のタイプかもしれないのだ。STL では静的な型チェックがあるが、Common Lisp 上で動作する CL-STL にはそんなものはない。だから、前述の operator= を defmethod する場合、同じタイプ同士で特定化するだけではいけない。たとえばコンテナの場合、同じタイプ同士なら「内容のコピー」をし、それ以外ならば別のことをしなければならないだろう。コピー先が nil だったらコンテナの複製を作ることになるかもしれないし、それ以外であれば例外を投げることになるかもしれない。

嗚呼、書いているうちに何をどうすればいいのか本気でわからなくなってきた。少し頭を冷やして、続きは改めて書くことにしよう。

 

コメント

このページにコメントする

 

このページのタグ

Page tag : Common Lisp

Page tag : STLとその移植

 

 


Copyright(C) 2005-2017 project-enigma.
Generated by CL-PREFAB.