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
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:
(defun my-caller (&rest 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"))
(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
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:
(defmacro my-caller (&rest args)
(if (cl-evenp (length args))
(error "Function `my-caller' requires an even number of arguments")))
(defun my-caller--function (&rest args)
;; function body goes here
(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
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.