Может ли функция или макрос определять предупреждения байтового компилятора?

15

Я пишу функцию, которая, в принципе, принимает произвольное количество аргументов. На практике, однако, ему всегда следует передавать только четное количество аргументов, и в противном случае это приведет к нежелательным результатам.

Вот фиктивный пример для контекста:

(defun my-caller (&rest args)
  (while args
    (call-other-function (pop args) (pop args))))

Когда файл elisp компилируется байтом, байтовый компилятор выдает предупреждение, когда видит, что функция вызывается с неправильным числом аргументов. Очевидно, что этого никогда не случится my-caller, так как он определен, чтобы принимать любое число.

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

  1. Есть ли способ сообщить байт-компилятору об этом ограничении?
  2. Если нет, возможно ли это с помощью макроса вместо функции?
Malabarba
источник
«... когда он видит функцию, вызываемую с неправильным числом аргументов»?
itjeyd

Ответы:

13

РЕДАКТИРОВАТЬ : Лучший способ сделать это в недавнем Emacs - определить макрос компилятора для проверки количества аргументов. Мой оригинальный ответ с использованием обычного макроса сохранен ниже, но макрос-компилятор лучше, потому что он не препятствует передаче функции во время выполнения funcallили applyво время выполнения.

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

(require 'cl-lib)

(defun my-caller (&rest args)
  (while args
    (message "%S %S" (pop args) (pop args))))

(define-compiler-macro my-caller (&whole form &rest args)
  (when (not (cl-evenp (length args)))
    (byte-compile-warn "`my-caller' requires an even number of arguments"))
  form)

(my-caller 1 2 3 4)
(my-caller 1 2)
(funcall #'my-caller 1 2 3 4)       ; ok
(apply #'my-caller '(1 2))          ; also ok
(my-caller 1)                       ; produces a warning
(funcall #'my-caller 1 2 3)         ; no warning!
(apply #'my-caller '(1 2 3))        ; also no warning

Обратите внимание, что funcallи applyтеперь можно использовать, но они обходят проверку аргументов макросом компилятора. Несмотря на свое название, компилятор макросы также , кажется , будет расширен в ходе «истолковано» оценки через C-xC-e, M-xeval-bufferтак вы получите ошибки в оценке, а также по составлению этого примера.


Исходный ответ следует:

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

(require 'cl-lib)

(defmacro my-caller (&rest args)
  (if (cl-evenp (length args))
      `(my-caller--function ,@args)
    (error "Function `my-caller' requires an even number of arguments")))

(defun my-caller--function (&rest args)
  ;; function body goes here
  args)

(my-caller 1 2 3 4)
(my-caller 1 2 3)

Попытка скомпилировать вышеупомянутое в файл не удастся ( .elcфайл не создается), с красивым кликабельным сообщением об ошибке в журнале компиляции, в котором говорится:

test.el:14:1:Error: `my-caller' requires an even number of arguments

Кроме того, можно заменить (error …)с , (byte-compile-warn …)чтобы произвести предупреждение вместо ошибки, позволяя продолжить компиляцию. (Спасибо Джордону за то, что указал на это в комментариях).

Поскольку макросы раскрываются во время компиляции, с этой проверкой не взимается штраф за время выполнения. Конечно, вы не можете запретить другим людям звонить my-caller--functionнапрямую, но вы можете по крайней мере объявить это как «частную» функцию, используя соглашение о двойных дефисах.

Заметный недостаток использования макроса для этой цели является то , что my-callerбольше не является функция первого класса , вы не можете передать его funcallили applyво время выполнения (или , по крайней мере , он не будет делать то , что вы ожидаете). В этом отношении это решение не так хорошо, как возможность просто объявить предупреждение компилятора для реальной функции. Конечно, использование applyсделает невозможным проверку количества аргументов, передаваемых функции во время компиляции, так что, возможно, это приемлемый компромисс.

Джон О.
источник
2
Предупреждения компиляции создаются с помощьюbyte-compile-warn
Джордон Биондо
Теперь я задаюсь вопросом, может ли это быть более эффективно достигнуто путем определения макроса компилятора для функции. Это устранит недостаток отсутствия applyили funcallмакропакета. Я попробую это и отредактирую свой ответ, если это работает.
Джон О.
11

Да, вы можете использовать, byte-defop-compilerчтобы фактически указать функцию, которая компилирует вашу функцию, byte-defop-compilerимеет некоторые встроенные тонкости, которые помогут вам указать, что ваши функции должны выдавать предупреждения, основываясь на количестве аргументов.

Документация

Добавьте форму компилятора для FUNCTION.Если функция является символом, тогда переменная "byte-SYMBOL" должна назвать используемый код операции. Если функция представляет собой список, первый элемент является функцией, а второй элемент является символом байт-кода. Второй элемент может быть нулем, что означает отсутствие кода операции. COMPILE-HANDLER - это функция, используемая для компиляции этого байта, или это могут быть сокращения 0, 1, 2, 3, 0-1 или 1-2. Если это ноль, то обработчик "byte-compile-SYMBOL.


использование

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

(byte-defop-compiler my-caller 2)

Теперь ваша функция выдаст предупреждения при компиляции с чем угодно, кроме 2 аргументов.

Если вы хотите дать более конкретные предупреждения и написать свои собственные функции компилятора. Посмотрите byte-compile-one-argи другие подобные функции в bytecomp.el для справки.

Обратите внимание, что вы указываете не только некоторую функцию для проверки, но и на самом деле компиляцию. Снова скомпилируйте функции в bytecomp.el, и вы получите хорошую ссылку.


Безопасные маршруты

Это не то, что я видел документально или обсуждали в Интернете, но в целом я бы сказал, что это неправильный путь. Правильный маршрут (IMO) будет состоять в том, чтобы написать ваши определения функций с описательными сигнатурами или использовать макрос, который будет выдавать предупреждения во время расширения, проверять длину ваших аргументов и использовать byte-compile-warnили errorдля отображения ошибок. Вам также может пригодиться eval-when-compileпроверка ошибок.

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

Опять же, кажется, на самом деле не документировано или не советовано из того, что я видел (может быть неправильно), но я предполагаю, что шаблон, который будет здесь следовать, будет указывать какой-то заголовочный файл для вашего пакета, который полон кучей пустых defuns и звонки byte-defop-compiler. В основном это будет пакет, который требуется до того, как ваш реальный пакет может быть скомпилирован.

Мнение: Исходя из того, что я знаю, что немного, потому что я только что узнал обо всем этом, я бы посоветовал вам никогда не делать ничего из этого. Когда-либо

Джордон Биондо
источник
1
Связанный: есть bytecomp-упрощение, которое учит дополнительные предупреждения байтового компилятора.
Уилфред Хьюз