Продвинутый символ-макролет - PullRequest
1 голос
/ 08 апреля 2019

Предположим, у меня есть класс class со слотами first и second. Внутри моей функции я могу привязать переменную к одному из таких слотов, как

(symbol-macrolet ((var (first cls)))
 ....)

Очевидно, я также могу привязать второй слот к чему-либо.

Вопросы, скажем, что первое и второе - это либо некоторое число, либо nil. Скажем также, что если секунда не nil, первая всегда nil. Теперь я могу связать мою переменную с не-nil только одним макросом? Так что он просто смотрит на экземпляр данного класса и затем проверяет, является ли секунда nil. Если нет, он связывает var со вторым, иначе с первым.

Кажется сложным, но я почти уверен, что это можно сделать, просто не знаю, с чего начать.

Для дальнейшего обобщения - возможно ли связать переменную не с одним местом, а с одним конкретным набором, в зависимости от некоторого состояния?

Ответы [ 3 ]

2 голосов
/ 08 апреля 2019

Я думаю, что это не совсем просто. Вы могли бы сделать что-то вроде этого, которое работает только для чтения (я использовал поддельную структуру toy, поэтому мой код работает, что приведено здесь):

(defstruct toy
  (first nil)
  (second nil))

(defun foo (a-toy)
  (symbol-macrolet ((x (or (toy-first a-toy) (toy-second a-toy))))
    ...))

Но теперь (setf x ...) ужасно незаконно. Вы можете обойти это, как только вы определились с тем, что (setf x ...) должно делать , определив некоторые локальные функции. Здесь я решил, что он должен установить не-1009 * слот, поскольку это имеет смысл для меня.

(defun bar (a-toy)
  (flet ((toy-slot (the-toy)
           (or (toy-first the-toy) (toy-second the-toy)))
         ((setf toy-slot) (new the-toy)
           (if (toy-first the-toy)
               (setf (toy-first the-toy) new)
             (setf (toy-second the-toy) new))))
    (symbol-macrolet ((x (toy-slot a-toy)))
      (setf x 2)
      a-toy)))

И теперь вы можете обернуть все это в один макрос:

(defmacro binding-toy-slot ((x toy) &body forms)
  (let ((tsn (make-symbol "TOY-SLOT")))
    `(flet ((,tsn (the-toy)
              (or (toy-first the-toy) (toy-second the-toy)))
             ((setf ,tsn) (new the-toy)
               (if (toy-first the-toy)
                   (setf (toy-first the-toy) new)
                 (setf (toy-second the-toy) new))))
       (symbol-macrolet ((,x (,tsn ,toy)))
         ,@forms))))

(defun bar (a-toy)
  (binding-toy-slot (x a-toy)
    (setf x 3)
    a-toy))

Очевидно, вы можете обобщить binding-toy-slot, поэтому он, например, берет список имен доступа к слотам или что-то в этом роде.

Возможно, есть и лучшие способы сделать это, о которых я не думал: могут быть хитрые трюки с расширениями setf, которые позволят вам сделать это без маленьких вспомогательных функций. Вы также можете иметь вспомогательные функции global , которые передают объект и список методов доступа, которые могут попробовать сделать код немного меньше (хотя вы, вероятно, можете добиться такого же небольшого кода в любой серьезной реализации, объявив встроенные помощники). что должно привести к их полной компиляции).


Альтернативный и, возможно, лучший подход заключается в определении протокола, которого вы хотите достичь, с помощью универсальных функций. Это означает, что вещи определены глобально, и это связано с ответом Каз, но не совсем так.

Итак, скажем, у меня есть некоторый класс (это может быть структура, но если сделать ее полноценной standard-class, то у нас будут свободные слоты, что приятно):

(defclass toy ()
  ((first :initarg :first)
   (second :initarg :second)))

Теперь вы можете определить обобщенные функции с именами, такими как appropriate-slot-value & (setf appropriate-slot-value), или вы можете определить GF, который возвращает name соответствующего слота, например:

(define-condition no-appropriate-slot (unbound-slot)
  ;; this is not the right place in the condition heirarchy probably
  ()
  (:report "no appropriate slot was bound"))

(defgeneric appropriate-slot-name (object &key for)
  (:method :around (object &key (for ':read))
   (call-next-method object :for for)))

(defmethod appropriate-slot-name ((object toy) &key for)
  (let ((found (find-if (lambda (slot)
                          (slot-boundp object slot))
                        '(first second))))
    (ecase for
      ((:read)
       (unless found
         (error 'no-appropriate-slot :name '(first second) :instance object))
       found)
      ((:write)
       (or found 'first)))))

И теперь пара функций доступа может быть простой функцией, которая будет работать для любого класса, где есть метод для appropriate-slot-name:

(defun appropriate-slot-value (object)
  (slot-value object (appropriate-slot-name object :for ':read)))

(defun (setf appropriate-slot-value) (new object)
  ;; set the bound slot, or the first slot
  (setf (slot-value object (appropriate-slot-name object :for ':write)) new))

Наконец, теперь у нас могут быть функции, которые просто используют symbol-macrolet очевидным образом:

(defun foo (something)
  (symbol-macrolet ((s (appropriate-slot-value something)))
    ... s ... (setf s ...) ...))

Итак, это другой подход.

1 голос
/ 09 апреля 2019

Простой, неэффективный способ с defsetf:

(defun second-or-first (list)
  (or (second list) (first list)))

(defun set-second-or-first (list val)
  (if (second list)
    (setf (second list) val)
    (setf (first list) val)))

(defsetf second-or-first set-second-or-first)

(defun test ()
  (let ((list (list nil nil)))
    (symbol-macrolet ((sof (second-or-first list)))
      (flet ((prn ()
               (prin1 list) (terpri)
               (prin1 sof) (terpri)))
        (prn)
        (setf sof 0)
        (prn)
        (setf sof 1)
        (prn)
        (setf (second list) 3)
        (prn)
        (setf sof nil)
        (prn)
        (setf sof nil)
        (prn)))))

Если все в порядке, когда выражения обновления, такие как (incf sof), дважды расточительно обходят структуру, этого достаточно.

В противном случае требуется более сложная реализация с использованием define-setf-expander.Суть такого решения состоит в том, что сгенерированный код должен вычислить, какая из двух cons-ячеек списка занимает текущее место, сохраняя эту cons-ячейку во временной переменной #:temp.Тогда место, которое нас интересует, обозначается (car #:temp).Если #:temp - вторая ячейка, избегать двух сложных обращений (один доступ для определения того, какой мы хотим, а другой для получения предыдущего значения).По сути, мы можем использовать другую временную переменную, которая содержит значение места, которое мы получили в качестве побочного эффекта проверки, не является ли оно nil.Затем назначьте эту временную переменную в качестве формы доступа для получения предыдущего значения.

0 голосов
/ 14 апреля 2019

Вот как вы можете не использовать символьные макросы без каких-либо огромных потерь:

(defgeneric firsty-secondy (thing))
(defgeneric (setf firsty-secondy) (newval thing))
(defmethod firsty-secondy ((x my-class))
  (or (secondy x) (firsty x)))
(defmethod (setf firsty-secondy) (nv (x my-class))
  (if (secondy x)
      (setf (secondy x) nv)
      (setf (firsty x) nv)))

Вы можете обнаружить, что компилятор справляется с этим лучше, потому что в методах он может быть более уверен в том, где расположены слоты дляполя находятся в памяти.

Вот способ структурировать ваш объект так, чтобы вам не нужно было это делать, и немного лучше применяйте ваш инвариант:

(defclass my-class
  ((is-first :initform nil)
   (thingy :initform nil)))

Вот сравнение:

first=nil,second=nil  :  is-first=nil,thingy=nil
first=123,second=nil  :  is-first=t  ,thingy=123
first=nil,second=123  :  is-first=nil,thingy=123
first=123,second=456  : unrepresentable
...