#:g1: 実践SETF定義: setf placeって多値が取れたり取れなかったりする?

Posted 2018-12-12 13:04:44 GMT

Lisp SETF Advent Calendar 2018 12日目 》

前回の(setf all)を定義していて気付いたのですが、incfや、pushpopvaluesと組み合わせると処理系によって異なる動作をします。

下記のような例は、SBCLやAllegro CLでは中途半端な動きをしますが、LispWorksではマクロ展開時にエラーになります。

(let ((x 0)
      (y 0))
  (incf (values x y))
  (list x y))(1 nil) or error

(let ((x (list 0 1 2)) (y (list 1 2 3))) (pop (values x y)) (list x y))((1 2) nil) or error

もしや、setf以外はvaluesを取れないのかと思い、HyperSpecを確認してみましたが、5.1.2.3 VALUES Forms as Placesでもvaluessetf以外では機能しない、とは書いてありません。

間接的な定義がされている場合もあるので、それらしきものを探してみましたが、setfベースのマクロについての定義があります。

decfpoppushnewincfpushremfdefine-modify-macroで定義されているような挙動をする的な解釈が成立しそうな雰囲気もありますが、 define-modify-macroのAPIからすると、関数の引数として値を処理するので、多値は扱えません。
define-modify-macroで定義すれば、incfのようなものは、

(let ((x 0)
      (y 0))
  (setf (values x y)
        (values (1+ (values x y))))
  (list x y))(1 nil) 

のように展開されると思われるので、中途半端な返り値になっている理由も合点はいきます。

多値を扱いたい

とりあえず、多値を扱えるフォームはどんな感じになるか定義して眺めてみます。

incfの多値版は、こんな感じに定義できるかなと思います。

(defmacro incf* (place &optional (delta 1) &environment env)
  (multiple-value-bind (dummies vals newval setter getter)
                       (get-setf-expansion place env)
    (declare (ignore dummies vals setter))
    (let ((deltas (loop :for () :on newval :collect (gensym))))
      `(multiple-value-bind ,newval
                            ,getter
         (setf (values ,@deltas) 
               ,(if (eql 1 delta)
                    `(values ,@(loop :for () :on deltas :collect 1))
                    delta))
         (setf ,getter
               (values ,@(mapcar (lambda (v d)
                                   `(incf ,v ,d))
                                 newval
                                 deltas)))))))

(let ((x 0) (y 1)) (incf* (values x y) (values 1 10)) (list x y))(1 11)

popの多値版はこんな感じ

(defmacro pop* (place &environment env)
  (multiple-value-bind (dummies vals newval setter getter)
                       (get-setf-expansion place env)
    (declare (ignore dummies vals setter))
    (let ((retvars (loop :for () :on newval :collect (gensym))))
      `(multiple-value-bind ,newval
                            ,getter
         (let (,@(mapcar (lambda (a d)
                           `(,a (car ,d)))
                         retvars
                         newval))
           (setf ,getter
                 (values ,@(mapcar (lambda (v)
                                     `(cdr ,v))
                                   newval)))
           (values ,@retvars))))))

(let ((x (list 0 1 2)) (y (list 0 1 2))) (pop* (values x y)) (list x y))((1 2) (1 2))

VALUES Forms as Placesを処理できるdefine-modify-macro*のようなものを定義してみても良いかもしれません。

まとめ

どうせコンパイル時に展開すると思うのでVALUES Forms as Placesを処理できても良いと思うのですが、何か事情があるのでしょうか。

これらのフォームは、ループ内で頻出しそうなので極力無駄のない定義になる必要があるような気はしますが、展開時に無駄は省けそうですし、さて真相やいかに……。


HTML generated by 3bmd in LispWorks 7.0.0

comments powered by Disqus