Using macros to modify function bodies

I’m coming from Python, attempting to build a Julia equivalent of huggingface/knockknock. Briefly: KnockKnock decorates Python functions notifying you when said functions begin execution and fail/complete execution. My goals are approximately the same, just with Julia. (I realize this is attempting to use @macros as @decorators, and that they are distinct ideas with differing goals.)


  1. Can @macros modify the function body, define the function, and return said function – but with the redefined body without needing to define a custom function? (One of my many attempts appears to fail at this.)
  2. “Meta question”: Would achieving the above present capability issues with chaining macros? (From my own searching, I’ve vaguely grasped “yes”.)
    1. If so, would instead creating a syntax of… @knockknock <messaging-client> func() at-all sidestep this? (<messaging-client> would be something like Telegram or whatever else.)

Attempting to produce a @macro that does (1)

The general problems I’ve run in to are the following:

  1. KnockKnock-specific functions are executed when defining functions, instead of at run-time of the function.
  2. When modifying the function definitions (i.e. def[:body]), I seem unable to actually “define” the function in Julia such that calling foo(bar; baz=12) actually works.

Of the many variants I’ve tried, I believe this snippet generally represents what I’m trying to do:

macro knockknock(func::Expr)
    def = MacroTools.splitdef(func)
    body = def[:body]

    def[:body] = quote
        # KnockKnock specific function, properly imported
        local t0 = setup(def)
        # Your function body (which is represented as a `quote ... end`)
        local retval = $body
        # KnockKnock specific function, properly imported
        local (t1, tt) = teardown(def, t0, retval)
        return retval

   func = MacroTools.combinedef(def)

Then using something equivalent to this:

@knockknock function test()

Running the above prints the output of setup and teardown. Then, running test() produces an UndefVarError (in the REPL is you’ll receive something like #32#test (generic function with 1 method)).

I’ve been basing much of this off the @gen macro from Gen.jl. Following it’s execution leads me to this, which appears to use quote ... end to define the original function, then Gen's DynamicDSLFunction. The main caveat here is that the DynamicDSLFunction needs to be run through Gen's library functions which seem to use dispatch to call the DynamicDSLFunction variant (which seems undesirable given my goals are to make usage require minimal changes to existing code (and not require knowing much about how the function is executed)).

1 Like

It doesn’t exactly answer your macro questions, but have you considered just using closures instead to do this? Quite often they get you far enough that you don’t have to go messing with macros, though maybe I’ve overlooked something in the knockknock lib that requires that level of metaprogramming…

julia> setup(f) = nothing
setup (generic function with 1 method)

julia> teardown(f, t0, retval) = nothing, nothing
teardown (generic function with 1 method)

julia> function knockknock(f)
           function (args...; kws...)
               t0 = setup(f)
               retval = f(args...; kws...)
               t1, tt = teardown(f, t0, retval)
               return retval
knockknock (generic function with 1 method)

julia> f(x) = x + 1
f (generic function with 1 method)

julia> setup(::typeof(f)) = @show :before
setup (generic function with 2 methods)

julia> teardown(::typeof(f), t0, retval) = @show t0, retval
teardown (generic function with 2 methods)

julia> kkf = knockknock(f)
#1 (generic function with 1 method)

julia> kkf(1)
:before = :before
(t0, retval) = (:before, 2)

The @show calls in the setup and teardown definitions are there just for illustration, you can define whatever you what in those methods. The ::typeof(f) dispatches specifically on the type of the f function, if you’ve not come across that syntax before.