I'm new to elisp, and I'm struggling to figure out how to do advice. I got my code working, but I'm not sure why.
This is from a real problem that I had digging into markdown mode to add an extra exclusion to spellchecking to its implementation of flyspell-generic-word-predicate
---but the details don't matter, so I'll just reduce it to an abstract form to start with, and see if there's an easy explanation... but then I'll provide the full details later, in case my abstract theory of what's going on is wrong.
Consider a predicate function is-foo
that takes zero arguments and runs a test against the word at point.
Suppose I decide that the implementation of some library function is-foo
is deficient for my purposes, because there's a small, easily testable, class of words for which is-foo
returns t
but I'd prefer it returns nil
. So I write a predicate function of my own, which also takes no arguments---call it is-bar
---which correctly handles the case that is-foo
screws up.
And then I decide that the best way to handle matters is to just use advice to wrap is-bar
around is-foo
such that if is-bar
returns nil
, the whole function just returns nil
; otherwise, it returns whatever is-foo
would return.
So here's my first try:
(defun do-better-advice (orig)
(if (is-bar)
(orig)
nil))
(advice-add 'is-foo :around #'do-better-advice)
When I try to actually run the underlying code, what I get is an error about (void-function orig)
However, with the following small change, my code runs correctly:
(defun do-better-advice (orig &rest args)
(if (is-bar)
(apply orig args)))
What I don't understand is why calling apply
makes this work? The documentation for apply says that "apply calls function with arguments" and that "apply returns the result of calling function." But if that's true, then when f
takes no arguments, and is passed none (i.e. args
above is an empty list), then (f)
and (apply f args)
ought to evaluate to exactly the same thing.
So why don't they?
nitty-gritty details
In real life, this was an attempt to get flyspell to stop marking pandoc citation references (which begin with an ampersand) as spelling errors in markdown-mode. The source of the function I was advising, markdown-flyspell-check-word-p
is here; as you can see, it is a predicate that takes no arguments. Ultimately, the markdown-mode library just takes markdown-flyspell-check-word-p
and binds it to flyspell-generic-check-word-predicate
.
My code that failed was:
(defun is-ampersand (s)
(string= "@" s))
(defun not-cite ()
(save-excursion
(forward-word -1)
(let ((result (is-ampersand (string (preceding-char)))))
(not result))))
(defun not-cite-advice (orig)
(if (not-cite)
(orig)
nil))
(advice-add 'markdown-flyspell-check-word-p :around #'not-cite-advice)
And the specific error I got when I turned on flyspell was Error in post-command-hook (flyspell-post-command-hook): (void-function orig)
But when I simply rewrote to use apply and to pass it a presumably empty list of args, it worked exactly as intended:
(defun not-cite-advice (orig &rest args)
(if (not-cite)
(apply orig args)))
The only reason I actually knew to try apply
was because I saw a couple examples of using :around
in advice, and they all used apply
. The documentation for around just says "Call function instead of the old function, but provide the old function as an extra argument to function" --- it doesn't say anything like "and also pass around a list of the arguments" or anything like that. Although the quoted bit below I guess means that you can give it a list of args (?).