albep
albep

Reputation: 153

Write Julia macro that returns a function

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

Upvotes: 8

Views: 647

Answers (1)

phipsgabler
phipsgabler

Reputation: 20970

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.

Upvotes: 4

Related Questions