Generating function from existing function body + custom return statement

Hello,

I’m exploring an idea which may look funny. Please tell me if you thinks it’s impossible or useless.

My goal

I’d like to generate automatically (with metaprogramming I guess) functions which body and signature comes from an existing function “template” and then appending a return statement (return myvar where myvar can be decided in the code). Here is an illustration.

The function “template” model is a model of physical system which computes many variables (only 3 (c,d,e) in this dummy example).

function model(a,b)
    c = 2*a
    d = c+b
    e = d^2
end

To provide more context, this model will be used in several optimization variants, like:

  • minimize d
  • minimize d s.t. e ≤ 2
  • minimize e+d

These variants should be user selectable.
To support the variants without duplicating the code of model, I’d like an easy way for the user my optimization tool to select the relevant output(s) of model. So I’ve imagined I could have macro which could be invoked as:

@set_return model_retc model :c

And this would generate the function:

function model_retc(a,b) # function name set in macro invokation
    # signature and body copy-pasted from `model`
    c = 2*a
    d = c+b
    e = d^2
    return c # returned variable(s) set in macro invokation
end

It’s not clear to me how to do this. I’ve searched two approaches:

  1. Get the AST of the template function, but I kind of understood that AST is defined for specific methods rather than the generic function (SO: Access the AST for generic functions in Julia)
  2. Get the source code of the template function, but I didn’t go all the way. Only, I found on SO How to print in REPL the code of functions in Julia? which seems to indicate that it’s not working for REPL-defined functions.
  • Also, the @less macro is about opening the source in a pager, but I just want to get the source in a variable.

Also, I got from Write Julia macro that returns a function that to create a function in a macro, it is necessary to have the the content known at compile time, not at execution time. I think in my case all information is available in the source code.

Alternative without metaprogramming: is it good enough?

Since I don’t know how to achieve my goal with metaprogramming, I’m using an approach based on 1) returning all variables of potential interest and 2) select those in a wrapper.

The function which return “all” is:

function model_retall(a,b)
    c = 2*a
    d = c+b
    e = d^2
    return Dict(:c => c, :d => d, :e => e)
end

Then I can create (manually or with a short function factory):

function model_retc(a,b)
    model_retall(a,b)[:c]
end

My questions on this metaprogramming-less approach are:

  • Are there better alternatives than returning a Dict of variables?
    • This approach is actually insired by an existing Python tool which enforces a convention that all model functions should return locals(), i.e. all local variables?
  • Is there some performance issue related to returning many outputs kike in model_retall and then dropping most of them (e.g. d and e ends up being unnecessary to compute c)
    • in particular, in my optimization approach, I want to compute the gradient of model_retc with some AD package (ForwardDiff, Zygote, other…?)

You can return a named tuple:

julia> function model(a,b)
           c = 2 * a
           d = c + b
           e = d ^ 2
           return (c = c, d = d, e = e)
       end
model (generic function with 1 method)

julia> t = model(1, 2)
(c = 2, d = 4, e = 16)

julia> t.c
2
5 Likes

Definitely agreed–returning a NamedTuple should work very well in this case. Furthermore, you can write a function to select the relevant field, and you can even use that to create a new function which returns only the field of interest.

For example:

julia> function model(a,b)
           c = 2 * a
           d = c + b
           e = d ^ 2
           return (c = c, d = d, e = e)
       end
model (generic function with 1 method)

# A new function which calls `model` and then returns the `c` from its named tuple:
julia> model_c(args...) = model(args...).c
model_c (generic function with 1 method)

julia> model_c(1, 2)
2
3 Likes

Thanks for the tip! The dynamic and mutable nature of a Dict is certainly not needed in my case.