2014-04-22-01-本気で move について考える - 2 - project-enigma

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 の約束事は以下の通りだ。

当初、__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-2017 project-enigma.
Generated by CL-PREFAB.