6

I've been trying to a macro to put one-time advice on a function.

What I mean by one-time advice is advice which removes itself after it is called – so that when you add the advice and run the original function, the advice runs, but if you run the function again, the advice is gone.

My current (non-working) definition is

(defmacro add-one-time-advice (symbol where function &optional props)
  `(let ((ad-sym (make-symbol "advice")))
    (fset ad-sym (symbol-function ,function))
    (advice-add ad-sym :after (lambda () (advice-remove ,symbol ad-sym)))
    (advice-add ,symbol ,where ad-sym ,props)))

This throws no errors, but the advice doesn't remove itself.

If my approach is okay, how can I fix it, and if it's not, what's the cleanest way to implement this?

3 Answers 3

7

You don't need a macro for this. And you don't need (but you can certainly use) lexical binding.

;; Without lexical binding:
;;
(defun advise-once (symbol where function &optional props)
  (advice-add symbol :after `(lambda (&rest _) (advice-remove ',symbol ',function)))
  (advice-add symbol where function props))

;; With lexical binding:
;;
(defun advise-once (symbol where function &optional props)
  (advice-add symbol :after (lambda (&rest _) (advice-remove symbol function)))
  (advice-add symbol where function props))

(advise-once 'backward-word :before (lambda (_) (message "Eureka!")))

The reason you do not need lexical binding is that you are interested, when the function (lambda form) is used, only in the values of symbol and function, such as they were at the time the lambda-form list was created. You do not need variables symbol and function when the lambda form is used.

The use of the backquote syntax constructs a list that is a lambda form, where the values of symbol and function have been substituted in place of those variables.

Using lexical binding has the advantage that the lambda form can be compiled, because it is recognized by the compiler as a function, whereas in the case of using the backquote syntax all the compiler sees is code that constructs a list.

1
  • I'm getting too macro-happy! This teaches me to try using a function first... I think the insight here is to use another advice to remove the first advice from the function (instead of advising the advice). Anyway, cheers.
    – digitalis_
    Commented Aug 14, 2016 at 10:33
1

The code looks OK, actually (I would get rid of the call to symbol-function, tho).

My crystal ball tells me that maybe you get bitten by dynamic scoping: your code relies on lexical-scoping for its use of ad-sym from within the lambda, so it's indispensable that lexical-binding be non-nil in the code that uses this macro.

0

As far as I understand, the proposed solution lets the function advised. The :after advice to remove the original advice is not removed, so (advice-remove ...) remains being executed.

Here is an alternative solution "modifying the advice". Now, no advice is left, but it just works for interactive advises. Hopefully someone would fix it :)

(defun advice-once (symbol where function &optional props)
  (let ((new-function (intern (concat (symbol-name function) "-advice-once"))))
    (fset new-function `(lambda (&rest _)
                   ;; (,function)
                   ;; (funcall ',function)
                   ;; (command-execute ',function)
                   (call-interactively ',function)
                   (advice-remove ',symbol #',new-function)))
    (advice-add symbol where new-function props)))

(defun eureka nil
  (interactive)
 (message "Eureka!"))

(advice-once 'backward-word :before #'eureka)

Following OP code, here is a version "advising the advice". The code is more simple, symbol does not remains advised and it works for pretty much any advising function. But, the advising function function remains advised, which can not be solved.

(defun advice-once (symbol where function &optional props)
    (advice-add function :after `(lambda (&rest _) (advice-remove ',symbol #',function)))
    (advice-add symbol where function props))

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.