объединение двух переменных в одно имя функции в макросе

Я играл с макросами и закрытием, где я создал макрос «объект» для создания экземпляров.

(defmacro object (class &rest args)
  `(make-instance ',class ,@args))

Теперь, делая это, я также хотел сделать что-то подобное для функций доступа, созданных clos. Пример:

(defclass person () ((name :accessor person-name :initarg :name)))

затем создание экземпляра

(setf p1 (object person :name "tom"))

теперь, чтобы получить имя из объекта, очевидно, я бы назвал имя человека, однако, как и в случае с макросом объекта, я хотел создать для этого макрос «получает». Так что в идеале:

(gets person name p1) which then would return the name.

Проблема тогда заключается в привязке человека и имени (человек-имя) и как это сделать. Есть ли способ связать эти два аргумента вместе в макросе? вроде как:

(defmacro gets (class var object)
  `(,class-,var ,object))

person Lalzy    schedule 26.06.2014    source источник
comment
можете ли вы объяснить, ПОЧЕМУ вы на самом деле создали менее полезный макрос object? Для меня это не имеет никакого смысла. Макрос gets тоже не имеет никакого смысла, ИМХО.   -  person Rainer Joswig    schedule 26.06.2014
comment
Как указано в других полях комментариев к ответам, я тестировал макросы не больше и не меньше. Макрос объекта должен был придать больше похожести на Java при создании классов, чтобы показать моему другу, что он любит синтаксис Java. Итак, еще раз, это никогда не было, никогда не будет и никогда не будет использоваться ни для чего, кроме как на 100% в процессе изучения и изучения того, как работают вещи.   -  person Lalzy    schedule 07.07.2014


Ответы (4)


Я думаю, что, возможно, неправильно понял первоначальный замысел. Сначала я подумал, что вы спрашиваете, как сгенерировать имена средств доступа для определения класса, к которому относится третья часть ответа. После прочтения во второй раз на самом деле звучит так, будто вы хотите сгенерировать новый символ и вызвать его с некоторым аргументом. Это тоже достаточно просто, и дается во второй части этого ответа. И вторая, и третья части зависят от возможности создать символ с именем, составленным из имен других символов, и с этого мы и начнем.

«Объединение» символов

У каждого символа есть имя (строка), которое можно получить с помощью symbol-name< /а>. Вы можете использовать конкатенацию, чтобы создать новую строку из некоторых старых строк, а затем используйте intern, чтобы получить символ с новым именем.

(intern (concatenate 'string
                     (symbol-name 'person)
                     "-"
                     (symbol-name 'name)))
;=> PERSON-NAME

Реконструкция имени аксессора

(defmacro gets (class-name slot-name object)
  (let ((accessor-name 
         (intern (concatenate 'string
                              (symbol-name class-name)
                              "-"
                              (symbol-name slot-name))
                 (symbol-package class-name))))
    `(,accessor-name ,object)))
(macroexpand-1 '(gets person name some-person))
;=> (PERSON-NAME SOME-PERSON)

Однако по ряду причин это не очень надежно. (i) Вы не знаете, есть ли у слота метод доступа вида <class-name>-<slot-name>. (ii) Даже если у слота есть аксессор вида <class-name>-<slot-name>, вы не знаете, в каком пакете он находится. В коде выше я сделал разумное предположение, что это то же самое, что и пакет имени класса, но это не так. совсем не требуется. Вы могли бы, например:

(defclass a:person ()
  ((b:name :accessor c:person-name)))

и тогда этот подход не будет работать вообще. (iii) Это не очень хорошо работает с наследованием. Если вы создадите подкласс person, скажем, с north-american-person, то вы все равно сможете вызвать person-name с помощью north-american-person, но вы не сможете вызвать north-american-person-name ни с чем. (iv) Это похоже на новое изобретение slot-value. Вы уже можете получить доступ к значению слота, используя только имя слота с помощью (slot-value object slot-name), и я не вижу причин, по которым ваш макрос gets не должен просто расширяться до этого. Там вам не придется беспокоиться о конкретном имени средства доступа (если оно вообще есть) или о пакете имени класса, а только о фактическом имени слота.

Генерация имен доступа

Вам просто нужно извлечь имена символов и сгенерировать новый символ с нужным именем.
Если вы хотите автоматически генерировать методы доступа с именами в стиле defstruct, вы можете сделать это следующим образом:

(defmacro define-class (name direct-superclasses slots &rest options)
  (flet ((%slot (slot)
           (destructuring-bind (slot-name &rest options) 
               (if (listp slot) slot (list slot))
             `(,slot-name ,@options :accessor ,(intern (concatenate 'string
                                                                    (symbol-name name)
                                                                    "-"
                                                                    (symbol-name slot-name)))))))
    `(defclass ,name ,direct-superclasses
       ,(mapcar #'%slot slots)
       ,@options)))

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

(pprint (macroexpand-1 '(define-class person ()
                         ((name :type string :initarg :name)
                          (age :type integer :initarg :age)
                          home))))

(DEFCLASS PERSON NIL
          ((NAME :TYPE STRING :INITARG :NAME :ACCESSOR PERSON-NAME)
           (AGE :TYPE INTEGER :INITARG :AGE :ACCESSOR PERSON-AGE)
           (HOME :ACCESSOR PERSON-HOME)))

И мы видим, что он работает так, как ожидалось:

(define-class person ()
  ((name :type string :initarg :name)
   (age :type integer :initarg :age)
   home))

(person-name (make-instance 'person :name "John"))
;=> "John"

Другие комментарии к вашему коду

(defmacro object (class &rest args)
  `(make-instance ',class ,@args))

Как указал Райнер, это не очень полезный. В большинстве случаев это то же самое, что и

(defun object (class &rest args)
  (apply 'make-instance class args))

за исключением того, что вы можете (funcall #'object …) и (apply #'object …) с функцией, но не можете с макросом.

Ваш макрос gets не более полезен, чем slot-value , который принимает объект и имя слота. Для него не требуется имя класса, и он будет работать, даже если у класса нет модуля чтения или доступа.

Не создавайте (наивно) имена символов с format

Я создавал имена символов с конкатенацией и именем символа. Иногда вы увидите, что люди используют формат для создания имен, например, (format nil "~A-~A" 'person 'name), но это может привести к проблемам с настройками заглавных букв, которые можно изменить. Например, ниже мы определяем функцию foo-bar и отмечаем, что подход, основанный на формате, не работает, но подход, основанный на конкатенации, работает.

CL-USER> (defun foo-bar ()
           (print 'hello))
FOO-BAR
CL-USER> (foo-bar)

HELLO 
HELLO
CL-USER> (setf *print-case* :capitalize)
:Capitalize
CL-USER> (funcall (intern (concatenate 'string (symbol-name 'foo) "-" (symbol-name 'bar))))

Hello 
Hello
CL-USER> (format nil "~a-~a" 'foo 'bar)
"Foo-Bar"
CL-USER> (intern (format nil "~a-~a" 'foo 'bar))
|Foo-Bar|
Nil
CL-USER> (funcall (intern (format nil "~a-~a" 'foo 'bar)))
; Evaluation aborted on #<Undefined-Function Foo-Bar {1002BF8AF1}>.

Проблема здесь в том, что мы не сохраняем регистр имен символов аргументов. Чтобы сохранить случай, нам нужно явно извлечь имена символов, а не позволять функциям печати сопоставлять имя символа с какой-либо другой строкой. Чтобы проиллюстрировать проблему, рассмотрим:

CL-USER> (setf (readtable-case *readtable*) :preserve)
PRESERVE

;; The symbol-names of foo and bar are "foo" and "bar", but 
;; you're upcasing them, so you end up with the name "FOO-BAR".
CL-USER> (FORMAT NIL "~{~A~^-~}" (MAPCAR 'STRING-UPCASE '(foo bar)))
"FOO-BAR"

;; If you just concatenate their symbol-names, though, you
;; end up with "foo-bar".
CL-USER> (CONCATENATE 'STRING (SYMBOL-NAME 'foo) "-" (SYMBOL-NAME 'bar))
"foo-bar"

;; You can map symbol-name instead of string-upcase, though, and 
;; then you'll get the desired result, "foo-bar"
CL-USER> (FORMAT NIL "~{~A~^-~}" (MAPCAR 'SYMBOL-NAME '(foo bar)))
"foo-bar"
person Joshua Taylor    schedule 26.06.2014
comment
Это именно то, что я искал (и многое другое), спасибо. Я прекрасно понимаю, что это не очень полезно в любой серьезной программе, но в основном это просто для того, чтобы увидеть возможности макросов и научиться их использовать. Остальная часть в значительной степени прошла мимо моей головы, но я обязательно вернусь к ней, когда привыкну к макросам. - person Lalzy; 26.06.2014

Эта функция создает символы из обозначений строк:

(defun symb (&rest args)
  (intern (format nil "~{~a~^-~}" (mapcar #'string args))))

Функция использует format, но проходит тест Джошуа:

CL-USER> (symb 'foo :bar "BAZ")
FOO-BAR-BAZ
NIL
CL-USER> (defun foo-bar ()
           (print 'hello))
FOO-BAR
CL-USER> (foo-bar)

HELLO 
HELLO
CL-USER> (setf *print-case* :capitalize)
:Capitalize
CL-USER> (funcall (symb 'foo 'bar))

Hello 
Hello
person Mark Karpov    schedule 27.06.2014
comment
Вы больше не получаете символ, имя которого формируется из имен символов входных аргументов. Например, если вы вызовете (symb '|foo| '|bar|), вы получите символ с именем "FOO-BAR", а не символ с именем "foo-bar". Если вы хотите использовать формат, просто сопоставьте symbol-name с входными символами (или string с аргументами). Важно сохранить регистр в именах символов. - person Joshua Taylor; 27.06.2014
comment
@ Джошуа, действительно, я исправил свою функцию. Спасибо! - person Mark Karpov; 27.06.2014
comment
Я также обновил свой ответ, приведя пример того, как, например, читаемый регистр может привести к проблеме с подходом с верхним регистром. - person Joshua Taylor; 27.06.2014
comment
из строк, символов и/или ключевых слов Это слишком специфично и немного неточно. string-upcase (из первой версии) и string берут обозначение строки, которое представляет собой строку, символ (возможно, ключевое слово, которое является просто символом в пакете ключевых слов) или характер. Так что вы можете сделать, например, (symb 'foo #\- :bar). - person Joshua Taylor; 27.06.2014

Если вы хотите, чтобы ваш gets использовал методы доступа:

(defmacro gets (class var object)
  `(,(intern (format nil "~a-~a" (symbol-name class) (symbol-name var))) ,object))

В общем, то, что вы пытаетесь сделать, не очень полезно. make-instance — хорошо известный символ, который легко найти, является частью стандарта и оптимизирован некоторыми реализациями, когда имя класса является постоянным. Итак, с вашим макросом object вы просто сохраняете несколько символов и одинарную кавычку. Обычно make-instance скрывают в определенных случаях, когда вы не хотите предоставлять прямой способ инициализации экземпляров или, что более вероятно, когда вы хотите предоставить уровни инициализации (например, фазы инициализации, слоты Лиспа и посторонние объекты).


PS: я смутно припоминаю, что кто-то видный специалист по стандартизации Common Lisp выступал за всегда обертывание/скрытие make-instance в функции (например, make-<class-name>), но я не могу найти ни ссылки, ни аргументации .


PPS: Вот довольно старая дискуссия (2004 г.) об этом в comp. lang.lisp (и еще один из 2002 года). Основные причины, по которым люди говорят в пользу функций-конструкторов:

  1. обязательные аргументы; достижимо во время выполнения, а не во время компиляции с :initform (error ...) в слоте, который требует предоставленного начального значения

  2. Как правило, скрывать детали реализации: экземпляр класса, экземпляр структуры, минусы, что-то еще

    2.1. Не желая экспортировать фактическое имя класса

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

  3. Удобное сокращение для определенного класса

Я вычеркиваю всегда, потому что кажется, что сторонники функций-конструкторов для объектов CLOS не обязательно хотят скрывать протокол, которому make-instance следует (allocate-instance, initialize-instanceshared-initialize) для разработчиков или расширителей API или фреймворка, хотя они могут захотеть скрыть это от потребителей API или фреймворка.


Для чего-то более быстрого вам может потребоваться прямой доступ к слотам, но при этом не используются методы доступа и, следовательно, не поддерживаются побочные эффекты, например. :before и :after методы:

(defmacro gets (class var object)
  (let ((object-var (gensym)))
    `(let ((,object-var ,object))
       (declare (optimize (speed 3) (safety 0) (debug 0))
                (type ,class ,object-var))
       (slot-value ,object-var ',var))))

В некоторых реализациях это может быть прямой доступ к слоту.

Наконец, у вас также есть with-slots и with-accessors в стандарте.

person acelent    schedule 26.06.2014
comment
Как уже говорилось, я просто играл с макросами и т. д., чтобы посмотреть, что с ними можно сделать (у меня все еще есть огромная проблема с пониманием того, как работают макросы и т. д.), и не пытался создать что-либо полезное. Все в процессе обучения можно сказать. - person Lalzy; 26.06.2014
comment
Я добавил пример в конец моего ответа, который показывает, почему здесь не следует использовать формат. - person Joshua Taylor; 27.06.2014
comment
@JoshuaTaylor, хорошая мысль. Кроме того, неправильно не format как таковое, а ~a (или princ) с символом. Если бы вместо символа мы указывали его имя (либо symbol-name, либо string), было бы нормально. - person acelent; 27.06.2014
comment
Да, вы можете избежать многих проблем с форматированием, если в качестве аргументов укажете фактические строки. Однако, если вы хотите добавить какой-либо алфавитный текст, это становится сложнее. Например, (format nil COPY-~a ...) или (format nil copy-~a ...) и т. д. COPY-~a, скорее всего, будет работать хорошо, но (format nil ~a-~a ( string 'copy)...) по крайней мере даст вам имя любой копии, которая читается как. - person Joshua Taylor; 27.06.2014

Попробуйте поиграть с чем-то вроде этого:

(let ((a 'a)
      (dash '-)
      (b 'b))
 `(,a,dash,b))

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

person user1597986    schedule 26.06.2014
comment
Это чистая дезинформация, вы когда-нибудь пробовали такой код? Читатель не объединяет ,a,dash,b только потому, что вы не включили пробелы, вы должны сгенерировать имя символа. - person acelent; 26.06.2014