Как передать символ условия функции в макрос - PullRequest
0 голосов
/ 07 марта 2019

Я пытаюсь передать символ условия функции в макрос и посмотреть результат:

(defmacro macro-test-1 (form condition)
  `(handler-case (funcall ,form)
     (,condition (c)
       (declare (ignore c))
       (format t "~a" 'why?))))

(macro-test-1 #'(lambda () (error 'simple-type-error)) division-by-zero)
;; OK, I get the simple-type-error as expected.

(defun test-1 (condition)
  (macro-test-1 #'(lambda () (error 'simple-type-error)) condition))
; in: DEFUN TEST-1
;     (SB-INT:NAMED-LAMBDA TEST-1
;         (CONDITION)
;       (BLOCK TEST-1
;         (MACRO-TEST-1 #'(LAMBDA () (ERROR 'SIMPLE-TYPE-ERROR)) CONDITION)))
; 
; caught STYLE-WARNING:
;   The variable CONDITION is defined but never used.
; 
; compilation unit finished
;   caught 1 STYLE-WARNING condition
TEST-1
;; what happened?

(test-1 'division-by-zero)
WHY?
NIL
;; what happened?

Я довольно смущен тем, что происходит, я пытался понять это в течение долгого времени, я надеюсь, что упускаю что-то глупое.

до 1

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

На самом деле я тестировал здесь, переключаясь на функцию, которая все еще не работает:

CL-USER> (defun macro-test-1 (form condition)
  (handler-case (funcall form)
     (condition (c)
       (declare (ignore c))
       (format t "~a" 'why?))))

; in: DEFUN MACRO-TEST-1
;     (SB-INT:NAMED-LAMBDA MACRO-TEST-1
;         (FORM CONDITION)
;       (BLOCK MACRO-TEST-1
;         (HANDLER-CASE (FUNCALL FORM)
;                       (CONDITION (C) (DECLARE #) (FORMAT T "~a" 'WHY?)))))
; 
; caught STYLE-WARNING:
;   The variable CONDITION is defined but never used.
; 
; compilation unit finished
;   caught 1 STYLE-WARNING condition
WARNING: redefining COMMON-LISP-USER::MACRO-TEST-1 in DEFUN

CL-USER> (macro-test-1 #'(lambda () (error 'simple-type-error)) 'division-by-zero)
WHY?
NIL

Однако, когда вы переопределяете macro-test-1 как макрос и переопределяете test-1 как макрос:

CL-USER> (defmacro test-1 (condition)
  `(macro-test-1 #'(lambda () (error 'simple-type-error)) ,condition))
TEST-1

 CL-USER> (test-1 division-by-zero)
; Evaluation aborted on #<SIMPLE-TYPE-ERROR {1001BB8FF3}>.

Я до сих пор не уверен, почему функция не работает, правило оценки не должно оценивать все аргументы и затем передавать в тело функции оцененные аргументы? Потому что не работает?

до 2

Я понимаю, что handler-case не работает, потому что вам нужно знать ошибки во время компиляции, и передача condition в качестве аргумента функции времени выполнения не сможет узнать ошибку во время компиляции, поэтому он не Работа. И я подчеркиваю эту единственную причину, а не потому, что происходящие макросы имеют время компиляции, из-за вопроса, который я отметил ниже, который привел меня ко всему этому беспорядку и заставил меня поверить, что можно передать condition функцией. Я могу сделать это:

(defmacro macro-test-1 (fn value)
`(funcall ,fn ,value 1))

(macro-test-1 #'= 1)
;; => T it is OK

(defun test-1 (fn value)
(macro-test-1 fn value))

(test-1 #'= 1)
;; => why it is OK?

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

до 3

Я решил пойти глубже и попытался:

(defmacro macro-test-1 (fn value)
`(,fn ,value 1))

(macro-test-1 = 1)
;; => T it is OK

(defun test-1 (fn value)
  (macro-test-1 fn value))
; in: DEFUN TEST-1                      
;     (SB-INT:NAMED-LAMBDA TEST-1       
;         (FN VALUE)                    
;       (BLOCK TEST-1 (MACRO-TEST-1 FN VALUE)))
;                                       
; caught STYLE-WARNING:                 
;   The variable FN is defined but never used.
; in: DEFUN TEST-1                      
;     (MACRO-TEST-1 FN VALUE)           
; ==>                                   
;   (FN VALUE 1)                        
;                                       
; caught STYLE-WARNING:                 
;   undefined function: FN              
;                                       
; compilation unit finished             
;   Undefined function:                 
;     FN                                
;   caught 2 STYLE-WARNING conditions   
WARNING: redefining COMMON-LISP-USER::TEST-1 in DEFUN
TEST-1

Да, я знаю, что если я попытаюсь, как показано ниже, он не выйдет, как ожидалось:

(test-1 '= 1)
; Evaluation aborted on #<UNDEFINED-FUNCTION FN {1004575323}>. ;

Но я хотел найти способ заставить его работать, поэтому я попытался, пока не смог, сбросив macro-test-1 на:

(defmacro macro-test-1 (fn value)
  `(eval (,fn ,value 1)))
WARNING: redefining COMMON-LISP-USER::MACRO-TEST-1 in DEFMACRO
MACRO-TEST-1
(defun test-1 (fn value)
  (macro-test-1 fn value))
WARNING: redefining COMMON-LISP-USER::TEST-1 in DEFUN
TEST-1

(test-1 '= 1)
T

Конечно, это сработает только в handler-case или case, если я переопределю его макросы, что, на мой взгляд, не должно быть хорошей практикой, и при этом мне это не нужно, но мне нравится идти туда, где это не так, ну, тогда я учусь ошибаться.

1 Ответ

3 голосов
/ 07 марта 2019

Макросы - это преобразование кода. Таким образом, расширение может произойти уже тогда, когда вы оцениваете defun. например.

(defun test-1 (condition)
  (macro-test-1 #'(lambda () (error 'simple-type-error)) condition))

;; becomes this 

(defun test-1 (condition)
  (handler-case (funcall #'(lambda nil (error 'simple-type-error)))
    (condition (c) 
      (declare (ignore c)) (format t "~a" 'why?))) 

Теперь давайте просто скажем, что вы хотите проверить случай обработчика для simple-type-error. Вы напишите это так:

  (handler-case expression
    (simple-type-error () 
      (format t "~a" 'why?))) 

не

  (handler-case expression
    ('simple-type-error () 
      (format t "~a" 'why?))) 

Например. handler-case - это синтаксис, и это место не может иметь переменную, которая может быть оценена для некоторой ошибки, но должна быть спецификатором типа, и это, вероятно, обрабатывается системой во время компиляции. Вот почему вы получаете, что condition никогда не используется, так как ваш handler-case проверяет тип с именем condition, а не тот, который вы отправили в качестве аргумента condition.

Создание test-1 макроса фактически передает division-by-zero в качестве символа macro-test-1, и в результате получается:

(handler-case (funcall #'(lambda nil (error 'simple-type-error)))
  (division-by-zero (c) 
    (declare (ignore c)) 
    (format t "~a" 'why?)))

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

EDIT

В up2 вы спрашиваете, почему это работает:

(defun test-1 (fn value) 
  (macro-test-1 fn value))

Итак, мы просто выясним, что на самом деле спасено:

(macroexpand-1 '(macro-test-1 fn value))
; ==> (funcall fn value)
; ==> t

Таким образом, ваша функция становится такой:

(defun test-1 (fn value) 
  (funcall fn value))

handler-case был синтаксисом, который не принимал переменные или выражения в нужном вам месте, и поэтому он не работал, но он, конечно, будет работать для всех функций, включая funcall, так как он оценивает все аргументы.

Чтобы показать вам другой пример того, что не работает, это case:

(defun check-something (what result default-value value)
  (case value
    (what result)
    (otherwise default-value)))

case - это макрос, так что на самом деле происходит. Мы можем сделать macroexpand-1, чтобы увидеть:

(macroexpand-1 
  '(case value
    (what result)
    (otherwise default-value))
   )
; ==> (let ((tmp value))
;       (cond ((eql tmp 'what) result) 
;             (t default-value)))
; ==> t

Макрос ожидает, что значения регистра являются литералами и, следовательно, заключают их в кавычки, чтобы они никогда не оценивались. Результирующая функция, которую вы ясно видите, what никогда не используется, как и condition:

(defun check-something (what result default-value value)
  (let ((tmp value))
    (cond ((eql tmp 'what) result) 
          (t default-value))))

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

То же самое для функций. Вся причина, по которой у нас есть макросы, заключается в контроле над оценкой. Хороший пример того, что не может быть записано как функция, это if:

(defun my-if (predicate consequence alternative)
  (cond (predicate consequence)
        (t alternative)))

(my-if t 'true 'false)   ; ==> true    
(my-if nil 'true 'false) ; ==> false    

Но так как функции всегда оценивают свои аргументы, вы не можете сделать это:

(defun factorial (n)
  (my-if (<= n 1)
         1
         (* n (factorial (1- n)))))

Это никогда не остановится, поскольку, будучи функцией, все 3 аргумента всегда оцениваются, и (* n (factorial (1- n)))) выполняется, даже если n отрицателен и будет иметь бесконечную рекурсию. Вместо этого использование макроса заменит my-if полученным cond, и оба cond и if не будут оценивать все свои аргументы, а не будут иметь короткие замыкания на том, который соответствует истинному предикату.

Вы можете использовать macroexpand-1, чтобы проверить, действительно ли ваш код правильный. Вы должны быть в состоянии заменить вход с weput. При использовании macroexpand применяет расширение, пока оно больше не будет расширяться. Например. cond также будет расширен до вложенных if.

РЕДАКТИРОВАТЬ 2

С up3:

(defun test-1 (fn value)
  (macro-test-1 fn value))

Это та же проблема. Функция макроса получает fn и value в качестве привязок, и в результате получается:

(defun test-1 (fn value)
  (fn value))

Это могло бы сработать в Схеме, но в Common Lisp символы в последовательности операторов отличаются от других позиций. Таким образом, когда CL пытается найти функцию fn, она никогда не выглядит близко к переменной fn. Единственный способ решить эту проблему - использовать funcall, и тогда вам вообще не нужен макрос:

(defun with-1 (fn value)
  (funcall fn value 1))

(with-1 #'+ 10) ; ==> 11

Обратите внимание на префикс #'. Это коротко для (function ...), поэтому на самом деле (function +). function - это специальная форма, которая принимает символ аргумента и получает значение из пространства имен функции.

С помощью eval вы можете делать много вещей, но это идет с ценой.Он не будет оптимизирован и, возможно, даже просто интерпретирован, и может привести к ошибкам компиляции во время выполнения, а также к угрозам безопасности.Хорошим примером был интерактивный интерактивный рубин, который только что сделал eval, и он работал хорошо, пока кто-то не оценил код, который удалил все системные файлы.eval считается вредным и даже злом.В своей профессиональной карьере я видел, как eval использовался 3 раза специально.(2 раза в PHP, один в requirejs).В один из тех случаев я бросил вызов писателю, что может быть лучший способ сделать это.Конечно, и handler-case, и case будут работать с eval, поскольку вычисляемый код будет иметь правильный формат, но вы потеряете лексическую область видимости.например.

(let ((x 10))
  (eval '(+ x 1))); 
; *** EVAL: variable X has no value

Вы можете быть умным и делать это:

(let ((x 10))
  (eval `(+ ,x 1))) ; ==> 11

но что, если это был список или что-то еще, не самооценочное?

(let ((x '(a b)))
  (eval `(cons '1 ,x)))
; *** undefined function: a

Таким образомeval также имеет свои вызовы.Держите подальше для других целей, чем образование.

...