UndefVarError when calling macro inside function

It relates to this post but I’m still unclear about the correct use of esc in my case.

I’m willing to call a function func to all fields of a struct.

Here’s my macro :

macro call_on_fields(func, struct_)
  exprs = [:($struct_.$field) for field in fieldnames(eval(:(typeof($struct_))))]
  return :($func($(exprs...)))

Here’s the struct def :

mutable struct mystruct
  a :: Int
  b :: Int

When defining

function f()

  foo(args...) = sum(args)
  s            = mystruct(1, 2)
  @call_on_fields foo s


the REPL says

ERROR: LoadError: UndefVarError: s not defined
 [1] top-level scope
   @ none:1
 [2] eval
   @ ./boot.jl:373 [inlined]
 [3] eval(x::Expr)
   @ Base.MainInclude ./client.jl:453
 [4] var"@call_on_fields"(__source__::LineNumberNode, __module__::Module, func::Any, struct_::Any)
   @ Main ./REPL[4]:2
in expression starting at REPL[5]:6

Not answering your question, but as a rule of thumb if you’re using eval inside a macro you’re likely doing something wrong, as using eval defeats the purpose of using a macro. Maybe you shouldn’t use a macro to start with.


And to answer your question, if I understand what you wanted to do, you can do

julia> macro call_on_fields(func, struct_)
         return :( $(esc(func))(getfield($(esc(struct_)), field) for field in fieldnames(typeof($(esc(struct_))))) )
@call_on_fields (macro with 1 method)

julia> function f()

         foo = sum
         s   = mystruct(1, 2)
         @call_on_fields foo s

f (generic function with 1 method)

julia> f()

but again, this is more easily solved by using a plain function, rather than a macro.

1 Like

Thanks @giordano. I was actually trying to find a way to do

func(struct_) = func(struct_.field1, ..., struct_.fieldN).

I previously considered using array comprehension :

func(struct_) = func( [struct_.field for field in fieldnames(typeof(struct_))]... )

but it is allocating memory. That’s why I moved to metaprogramming.

Do you know of a non-allocating plain function that could do that ?

A macro does not change the fact that you’d allocate memory.

Don’t create a vector, use a generator. Which is what I did in the macro above.

julia> call_on_fields(f, s) = f(getfield(s, field) for field in fieldnames(typeof(s)))
call_on_fields (generic function with 1 method)

julia> call_on_fields(sum, mystruct(1, 2))

julia> @benchmark call_on_fields(sum, mystruct(1, 2))
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  1.384 ns … 12.575 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.432 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.454 ns ±  0.437 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

  ▃ ▄      █▆     ▁  ▂                                     ▂ ▁
  █▁█▄▁▁▁▁▁██▅▁▁▁▁█▁██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▃█ █
  1.38 ns      Histogram: log(frequency) by time     1.66 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

The call_on_fields function above is the plain-function version of the macro in my previous message. Which one do you find more readable? :slightly_smiling_face:

1 Like

Wow, I didn’t know about generators. Really handful (and more readable than the macro, for sure :grin:). One last question :

What if I want to do call_on_fields(f, s, args...) to call f not only to all fields of s but also to args... ?

Unless f is a linear operator, I’m afraid you’d have to create a vector which concatenates the fields of s with args....

1 Like

Hmm, I see. Thanks a lot for your help !