2014-04-22-01-本気で move について考える - 2
>> Site top >> weblog >> 月別アーカイブ >> 2014年04月のlog >> 2014-04-22-01-本気で move について考える - 2
最終更新日付:2014/04/22 23:50:00
本気で move について考える - 2
2014 年 04 月 22 日
昨日はとりあえず動作するコードを書くだけで精一杯だった。今日はその内容を見ていくことことにする。
まずは、昨日のコードをまとめて再掲‥‥‥はしないことにした。順番に見ていくことにしよう。まずはキモになるクラス、remove-reference だ。
(defclass remove-reference () ((closure :type :function :initarg :closure :accessor __rm-ref-closure)))
これは最初に move によって「移動元の場所」を封じ込めるクロージャを持ち運ぶために作成される。その作成は以下の move マクロが行う。ちなみに、昨日書いた時点で自分でも無様だなと思っていた :get は改め、gensym を使うように変えてある。
(defgeneric __move-3 (first last result)) (let ((g-get (gensym "GET"))) (defmacro move (&rest args) (let ((cnt (length args))) (if (= cnt 1) (multiple-value-bind (vars forms var set ref) (get-setf-expansion (car args)) `(__move-1 (let* (,@(mapcar #'list vars forms)) (lambda (&optional (op ',g-get)) (if (eq op ',g-get) ,ref (let ((,@var op)) ,set nil)))))) `(__move-3 ,@args)))))
パラメータ数が 3 だった場合にコールされる __move-3 は、アルゴリズム版の range move だ。一方でパラメータ数が1の場合、そのパラメータを閉じ込めるクロージャを作成して __move-1 に渡す。このクロージャは、パラメータなしでコールした場合は「現在の値」を返し、それ以外は指定された値を設定する。__move-1 は以下のような関数だ。総称関数ではない。
(defun __move-1 (closure) (let ((val (funcall closure))) (if (eq (type-of val) 'remove-reference) val (make-instance 'remove-reference :closure closure))))
基本的には、__move-1 は渡されたクロージャから remove-reference インスタンスを作成する。しかし、クロージャに閉じ込められているのがすでに remove-reference インスタンスであれば、それをそのまま返す。これによって、(move foo) とすれば foo に対する remove-reference が作成されるが、foo がすでに remove-reference の場合は空振りをする。作成されるレキシカルクロージャが無駄になるが、他に良い方法が見つからなかった。いずれにせよ、このようにして作成された remove-reference は他の関数に渡しても有効となる。
で、これをどう使うか。現時点では、opr= による代入で使うところを考えている。以前から書いている通り、opr= による代入は opr= マクロと総称関数 operator= で実装しているが、以下は代入元が remove-reference だった場合のメソッドである。
(defmethod operator= (a (b remove-reference)) (multiple-value-bind (lhs rhs) (__move-2 a (funcall (__rm-ref-closure b))) (funcall (__rm-ref-closure b) rhs) lhs))
ご覧の通り、上記のコードでは remove-reference インスタンスから取り出したクロージャから「現在の値」を取り出し、__move-2 というのをコールしている。これは総称関数で、名前からの類推に反して move マクロから展開されるものではない。これは operator= の代入元が remove-reference だった場合の転送メソッドであり、デフォルトのメソッドは以下のようになっている。
;; never expanded by move macro. (defgeneric __move-2 (lhs rhs)) (defmethod __move-2 (lhs rhs) (declare (ignore lhs)) (values rhs rhs))
上記の operator= と __move-2 の約束事は以下の通りだ。
- operator= における、代入元が remove-reference だった場合のメソッドでは、それが閉じ込めているクロージャから「現在の値」を取り出して代入先の現在の値とともに __move-2 に渡す。
- __move-2 はパラメータを2つとるが、それは代入先と代入元それぞれの「現在の値」であり、評価結果として「ムーブ代入後の代入元/先の値を多値で返す」ことが期待されている。
- __move-2 のデフォルト実装では、代入先の値を無視し、代入元の値を返す。
当初、__move-2 というのを導入することは考えていなかった。operator= 内で remove-reference の現在の値を取得し、それをもって再度 operator= をコールすれば良いだろうと思っていたのだ。しかし、それではムーブ代入からの rebind なのかそうでないのかが判別できないため、現在のようなかたちになっている。
では、動きを見てみよう。__move-2 のデフォルト実装を使用する場合、以下のように実際にはムーブは行なわれない。
* (let ((v1 nil) (v2 666)) (opr= v1 (move v2)) (values v1 v2)) => 666 666
では、__move-2 の動きを変えてみよう。文字列のムーブが発生した場合に、移動元に :moved と設定されるようにしてみる。以下のように。
(defmethod __move-2 ((lhs string) (rhs string)) (values rhs :moved)) * (let ((v1 "") (v2 "666")) (opr= v1 (move v2)) (values v1 v2)) => "666" :MOVED
本日の最後はおまけ。副作用を伴うような式を渡してもちゃんと動作するよね、という確認。こう考えると get-setf-expansion てスゴいねぇ。
* (let ((idx 3) (arr #("0" "1" "2" "3" "4" "5")) (val "")) (opr= val (move (svref arr (incf idx)))) (values val arr)) => "4" #("0" "1" "2" "3" :MOVED "5")
コメント
このページのタグ
Page tag : Common Lisp
Page tag : STLとその移植
Copyright(C) 2005-2018 project-enigma.
Generated by CL-PREFAB.