14

I'm writing a function which, in principle, takes an arbitrary number of arguments. In practice, however, it should only ever be passed an even number of arguments, and will produce undesirable results otherwise.

Here's a dummy example for context:

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

When an elisp file is byte-compiled, the byte-compiler throws a warning when it sees a function being invoked with the wrong number of arguments. Obviously, that's never going to happen with my-caller, since it's defined to take any number.

Still, maybe there's a symbol property I can set, or a (declare) form I can add to its definition. Something to notify the user that this function should only be given an even number of arguments.

  1. Is there a way to inform the byte-compiler of this restriction?
  2. If not, Is it possible with a macro, instead of a function?
1
  • "... when it sees a function being invoked with the wrong number of arguments"?
    – itsjeyd
    Commented Oct 20, 2014 at 11:23

2 Answers 2

13

EDIT: A better way to do this in recent Emacs is by defining a compiler macro to check the number of arguments. My original answer using a normal macro is preserved below, but a compiler-macro is superior because it does not prevent passing the function to funcall or apply at runtime.

In recent versions of Emacs, you can do this by defining a compiler-macro for your function which checks the number of arguments and produces a warning (or even an error) if it doesn't match. The only subtlety is that the compiler macro should return the original function call form unchanged for evaluation or compilation. This is done by using a &whole argument and returning its value. This could be accomplished like this:

(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

Note that funcall and apply can now be used, but they bypass argument checking by the compiler macro. Despite their name, compiler macros also seem to be expanded in the course of 'interpreted' evaluation via C-xC-e, M-xeval-buffer, so you will get errors on evaluating as well as on compiling this example.


Original answer follows:

Here is how you could implement Jordon's suggestion to "use a macro that will provide warnings at expansion time". It turns out to be very easy:

(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)

Trying to compile the above in a file will fail (no .elc file is produced), with a nice clickable error message in the compile log, stating:

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

You could also replace (error …) with (byte-compile-warn …) to produce a warning instead of an error, allowing compilation to continue. (Thanks to Jordon for pointing this out in comments).

Since macros are expanded at compilation time, there is no run-time penalty associated with this check. Of course, you cannot stop other people calling my-caller--function directly, but you can at least advertise it as a "private" function using the double-hyphen convention.

A notable disadvantage of using a macro for this purpose is that my-caller is no longer a first-class function: you can't pass it to funcall or apply at runtime (or at least it won't do what you expect). In that respect, this solution isn't quite as good as being able to simply declare a compiler warning for a real function. Of course, using apply would make it impossible to check the number of arguments being passed to the function at compile-time anyway, so perhaps this is an acceptable trade-off.

2
  • 2
    Compile warnings are created with byte-compile-warn Commented Oct 20, 2014 at 13:15
  • I now wonder if this could be more effectively accomplished by defining a compiler macro for the function. This would eliminate the disadvantage of not being to apply or funcall the macro wrapper. I will try it out and edit my answer if it works.
    – user1968
    Commented Oct 1, 2015 at 22:48
10

Yes you can using byte-defop-compiler to actually specify a function that compiles your function, byte-defop-compiler has some built in niceties to help you specify that your functions should yield warnings based on having a number of args.

Documentation

Add a compiler-form for FUNCTION.If function is a symbol, then the variable "byte-SYMBOL" must name the opcode to be used. If function is a list, the first element is the function and the second element is the bytecode-symbol. The second element may be nil, meaning there is no opcode. COMPILE-HANDLER is the function to use to compile this byte-op, or may be the abbreviations 0, 1, 2, 3, 0-1, or 1-2. If it is nil, then the handler is "byte-compile-SYMBOL.


Usage

In your specific case you could use one of the abbreviations to define that your function should be given two args.

(byte-defop-compiler my-caller 2)

Now you're function will give warnings when compiled with anything but 2 args.

If you want to give more specific warnings and write your own compiler functions. Look at byte-compile-one-arg and other similar functions in bytecomp.el for reference.

Note that you are not just specifying some function to handle validation, but actually the compiling too. Again compile functions in bytecomp.el will provide you with a good reference.


Safer Routes

This is not something I've seen documented or discussed online but overall I would say this is ill-advised a route to take. The correct route (IMO) would be to write your defuns with descriptive signatures or use a macro that will provide warnings at expansion time, checking the length of your args and using byte-compile-warn or error to show errors. It may also benefit your to make use of eval-when-compile to do error checking.

You'll also need for your function to be defined before it is ever used, and the call to byte-defop-compiler will need to be before the compiler gets to actual calls of your function.

Again, seems to not really be documented or advised from what I've seen (could be wrong) but I imagine the pattern to follow here would be to specify some sort of header file for your package that is full of a bunch of empty defuns and calls to byte-defop-compiler. This would basically be a package that's required before your real package can be compiled.

Opinion: Based on what I know, which is not much, because I just learned about all of this, I would advise you to never do any of this. ever

1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.