Using macros to explode a struct into a tuple of its members

So, I would like use a macro to convert a struct into a tuple of its members

let boo be the following

struct boo 
  a
  b
  c
end

and collect defined as follows

macro collect(x)
  :( tuple($x.a,$x.b,$x.c) )
end
@collect (macro with 1 method)

things seem to work if invoked from the REPL

P = boo(1,2,3)
@collect P
(1, 2, 3)

but if the macro is invoked within a function or within a let block I get the following error

let y = P
  @collect y
end
UndefVarError: y not defined

Stacktrace:

 [1] top-level scope at In[6]:2

 [2] include_string(::Function, ::Module, ::String, ::String) at ./loading.jl:1091

I must defintitely be missing something essential about macros, can anyone please help me to understand what am I doing wrong here,

many thanks in advance!
Andrea

You need to escape x inside the macro with esc(x), this is also explained in the manual on macro hygiene:

https://docs.julialang.org/en/v1/manual/metaprogramming/#Hygiene

Note that in this particular example, there is not really a reason to prefer a macro over a regular function. Functions OTOH, are a lot more composable than macros, so it is generally better to avoid macros for things that can be done in a function.

4 Likes

many thanks! I will look that up!
the reason for using macros here is on one hand to experiment, on the other hand I thought it might be faster since macros are substituted before compile, so there is really no overhead due to argument passing etc.

The macro is just generating the code tuple(y.a, y.b, y.c), which will then still have to be run at runtime, so it won’t be any faster than a function.

1 Like

I got it to work in this fashion,

macro collect(x)
  :($(esc(:($x.a, $x.b, $x.c))))
end
@collect (macro with 1 method)
P = boo(1,2,3)
a,b,c = @collect P
(1, 2, 3)
a,b,c = let y = P
  @collect y
end
(1, 2, 3)

but I definitely need to ponder it some more.

you are probably right about speed, but I also wanted to give macros a try

many thanks again!
Andrea

1 Like

Note that you can do this with a generated function which will be just as fast as the macro version and usable on any type:

julia> @generated fieldvalues(x) = Expr(:tuple, (:(x.$f) for f=fieldnames(x))...)

julia> P = boo(1,2,3);

julia> fieldvalues(P)
(1,2,3)

If you look at e.g.

@code_llvm (P -> (a,b,c = fieldvalues(P)))(P)
@code_llvm (P -> (a,b,c = @collect(P)))(P)

you can confirm the LLVM code is identical between the generated function and the macro so its just as fast.

4 Likes

See also this discourse thread, which discussed conversion to a NamedTuple and also recommended a generated function.

1 Like

this is very nice, thanks! Did not know about generated functions.

thanks, I’ll look into that. I need to learn more about the metaprogramming side of Julia.