Search code examples
clojureadvising-functions

Advising protocol methods in Clojure


I'm trying to advise a number of methods in one library with utility functions from another library, where some of the methods to be advised are defined with (defn) and some are defined with (defprotocol).

Right now I'm using this library, which uses (alter-var-root). I don't care which library I use (or whether I hand-roll my own).

The problem I'm running into right now is that protocol methods sometimes can be advised, and sometimes cannot, depending on factors that are not perfectly clear to me.

  1. If I define a protocol, then define a type and implement that protocol in-line, then advising never seems to work. I am assuming this is because the type extends the JVM interface directly and skips the vars.

  2. If, in a single namespace, I define a protocol, then advise its methods, and then extend the protocol to a type, the advising will not work.

  3. If, in a single namespace, I define a protocol, then extend the protocol to a type, then advise the protocol's methods, the advising will work.

What I would like to do is find a method of advising that works reliably and does not rely on undefined implementation details. Is this possible?


Solution

  • Clojure itself doesn't provide any possibilities to advice functions in a reliable way, even those defined via def/defn. Consider the following example:

    (require '[richelieu.core :as advice])
    
    (advice/defadvice add-one [f x] (inc (f x)))
    
    (defn func-1 [x] x)
    (def func-2 func-1)
    
    (advice/advise-var #'func-1 add-one)
    
    > (func-1 0)
    1
    
    > (func-2 0)
    0
    

    After evaluation of the form (def func-2 func-1), var func-2 will contain binding of var func-1 (in other words its value), so advice-var won't affect it.

    Eventhough, definitions like func-2 are rare, you may have noticed or used the following:

    (defn generic-function [generic-parameter x y z]
      ...)
    
    (def specific-function-1 (partial generic-function <specific-arg-1>))
    (def specific-function-2 (partial generic-function <specific-arg-2>))
    ...
    

    If you advice generic-function, none of specific functions will work as expected due to peculiarity described above.

    If advising is critical for you, as a solution that may work, I'd suppose the following: since Clojure functions are compiled to java classes, you may try to replace java method invoke with other method that had desired behaviour (however, things become more complicated when talking about replacing protocol/interface methods: seems that you'll have to replace needed method in every class that implements particular protocol/interface).

    Otherwise, you'll need explicit wrapper for every function that you want to advice. Macros may help to reduce boilerplate in this case.