Write Julia macro that returns a function
Asked Answered
P

1

8

First post here, thanks for reading!

Problem: I have a Vector{String} - call it A - where each element is a part of an equation, e.g. the first element of A is "x[1] - (0.8*x[1])". I would like to write a macro that takes as arguments i) a String - call it fn_name - with the name of a function, ii) the vector A, and returns a function named fn_name which looks like

function fn_name(f, x)
  f[1] = x[1] - (0.8*x[1])
  f[2] = (exp(x[4]) - 0.8*exp(x[3]))^(-1.1) - (0.99*(exp(x[4]) - 0.8*exp(x[4]))^(-1.1)*(1.0 - 0.025 + 0.30*exp(x[1])*exp(x[2])^(0.30 - 1.0)))
  f[3] = exp(x[2]) - ((1.0 - 0.025)*exp(x[2]) + exp(x[1])*exp(x[2])^0.30 - exp(x[4]))
  f[4] = x[3] - (x[4])
end 

where each rhs is one element of

A = ["x[1] - (0.8*x[1])", "(exp(x[4]) - 0.8*exp(x[3]))^(-1.1) - (0.99*(exp(x[4]) - 0.8*exp(x[4]))^(-1.1)*(1.0 - 0.025 + 0.30*exp(x[1])*exp(x[2])^(0.30 - 1.0)))", "exp(x[2]) - ((1.0 - 0.025)*exp(x[2]) + exp(x[1])*exp(x[2])^0.30 - exp(x[4]))", "x[3] - (x[4])"]

What I tried: my best attempt at solving the problem is the following

macro make_fn(fn_name, A)
    esc(quote
        function $(Symbol(fn_name))(f, x)
            for i = 1:length($(A))
                f[$i] = Meta.parse($(A)[$i])
            end
        end
    end)
end

which however doesn't work: when I run @make_fn("my_name", A) I get the error LoadError: UndefVarError: i not defined.

I find it quite hard to wrap my head around Julia metaprogramming, and while I'd be very happy to avoid using it, I think for this problem it's unavoidable.

Can you please help me understand where my mistake is?

Thanks

Psilocybin answered 17/12, 2020 at 12:4 Comment(1)
A very nice first question! Note that greetings and thankings are not deemed necessary here, and leaving them out is not considered impolite - a good question speaks for itself.Loment
L
4

Macros in this case are not only avoidable, but even inapplicable, unless A is literal known at compile time.

I can provide a solution using eval and some closures:

julia> function make_fn2(A)
           Af = [@eval(x -> $(Meta.parse(expr))) for expr in A]
           function (f, x)
               for i in eachindex(A, f)
                   f[i] = Af[i](x)
               end
               return f
           end
       end
make_fn2 (generic function with 1 method)

julia> fn_name = make_fn2(A)
#46 (generic function with 1 method)

julia> fn_name(zeros(4), [1,2,3,4])
4-element Array{Float64,1}:
  0.19999999999999996
 -0.06594092302655707
 49.82984401122239
 -1.0

with the restrictions that

  1. eval will evaluate the expressions in a global scope of the module where this is defined (so it is potentially a different scope that a scope of the calling function), and
  2. the newly created function will work only if you first return to global scope (i.e. it will not work if you try to run it within a function in which you have created it).

But I'd really recommend thinking about a better input format than strings.

Loment answered 17/12, 2020 at 12:36 Comment(5)
I fully agree with the recommendation and answer. Two small additional comments (in particular showing why this is undesirable): 1) eval will get evaluated in a global scope of the module (so it is a different scope that a scope of the calling function), 2) the newly created function will work only if you first return to global scope (i.e. it will not work if you try to run it within a function in which you have created it)Larue
Thanks @BogumiłKamiński, very true. I have added those points. (And indeed you very easily run into world age issues by chaning seemlingly innocent things, like not hoisting the definition of Af out of the inner function.)Loment
I might have a solution for point 1., but it's too long for a comment and not tested properly.Loment
@Loment I'll have a look at the linked solution here, as indeed evaluation in global scope is a big issue, but I agree that ideally one would not be using strings as inputs from the onset (I'm trying to find a work-around in order to use functions from a package someone else wrote that relies on include() of txt files to generate some other functions in the workspace)Psilocybin
You can use Base.invokelatest to avoid world age issues, but this prevents compiler optimizations.Piggish

© 2022 - 2024 — McMap. All rights reserved.