Is it possible to get the fieldnames of a struct inside a macro?

Hi all,

I was wondering if it is possible to expand or get fieldnames of a DataType inside a macro. My instinct says it is not possible, but maybe someone knows a trick or something…

struct DummyStruct
  a
  b
end

# return somehow the fieldnames in DummyStruct
@dummymacro(DummyStruct)

Please, notice that I want to build a function expression inside the macro using those fieldnames and that is why I need to get those fieldnames in the macro scope.

Thanks!

What about fieldnames?

Hi. Thank you for your reply.

However, please be aware that I want to get the fieldnames inside the macro.

I want to build a function expression inside the macro using those fieldnames.

Can you instead have the macro generate code that calls fieldnames?

1 Like

No. The macro only sees the symbol :dummyvariable but not what it points to (this is undefined at macro evaluation time). But the macro can create code which uses fieldnames to look up field-names.

3 Likes
julia> macro doit(T)
           fnames = fieldnames(eval(T))
           # better:
           # fieldnames(Core.eval(__module__, T))
           @show fnames
       end
@doit (macro with 1 method)

julia> struct S;a;b;end

julia> @doit S
fnames = (:a, :b)
(:a, :b)

See also Using eval inside macro? - #2 by yuyichao

2 Likes

Hi @Oscar_Smith and @mauro3. Thanks for your reply. I get your suggestion, but I need those fieldnames to build the expression of a function, I cannot use fieldnames inside the function to solve my problem.

For example, I want to create a function that uses handlers for variables defined in a struct in order to be able to evaluate an expression:

p -> begin

  # expand fieldnames in p
  a = p.a
  b = p.b

  # This expression is given by the user and I don't want to operate on it
  # because it might involve many things, i.e. I don't want to change it to 
  # p.a + p.b
  return a + b
end

Using fieldnames in this function does not solve the problem.

Any insight is welcome. Thanks!

Hi @jw3126, thanks for your reply. This is really really helpful!

I can even use @mauro3 package Parameters.jl to define a struct:

julia> @doit(@with_kw struct foo 
         a = 1
         b = 2
       end)
(:a, :b)

That looks like a situation, where you might want to use a generated function instead of a macro. At least to me, that seems more idiomatic than calling eval inside a macro.

1 Like

An example is welcome!

I will think about it as well.

See @generated function staticschema:
https://github.com/JuliaArrays/StructArrays.jl/blob/master/src/interface.jl

Or @generated foreachfield:

https://github.com/JuliaArrays/StructArrays.jl/blob/master/src/utils.jl

Actually you can use foreachfield on a type declared in method signature (where {T} syntax)

1 Like

No, for exactly the same reasons as in Unpack named tuples (does anyone know how to do it?) - #9 by rdeits

The macro is, in general, expanded before the struct exists and certainly before the symbol being passed into the macro has a value. This just isn’t something macros can do for you, at least not in a way you can rely on.

An @generated function is probably the solution here, although it’s hard to know without knowing what you’re actually trying to accomplish. Can you give any more context?

3 Likes

I thought that parametric methods are specialized in a way that you have “unrolled” fieldnames code on types that are in method parmeters. Just because nothing prevents from optimizing it at compile time.

What about @jw3126 response? It seems correct to me. Can you check it out?

Yes, of course. I want to create a function expression inside a macro by receiving a DataType and a expression that needs to be evaluated using those attributes inside the struct.

So for example:

# define the struct with defaulted values
@with_kw struct foo{T}
  a::T = 1
  b::T = 2
end

f = @bar begin
   params: foo
   expr: a + b
end

where the macro bar builds the function expression:

foo -> begin

  # expand fieldnames in p
  a = foo.a
  b = foo.b

  # This expression is given by the user and I don't want to operate on it
  # because it might involve many things, i.e. I don't want to change it to 
  # foo.a + foo.b
  return a + b
end

I know that I can define the parameters inside the macro bar, which will allow me to know the parameters. However, I wanted to make them separatelye because they might be in separate windows in the front end (User Interface).

Thanks!

Sorry, I’m on a phone right now so I can’t run code, but try something like this:

function foo(T)
  @doit(T)
end

The macro just gets the symbol T. It has no access to the value that T is bound to when you call foo. After all, you haven’t even called foo yet, so how could it?

The given example happens to work because it only involves global variables that were already defined at the time the macro was expanded. You can’t rely on that being the case.

2 Likes

As for your actual task, am I correct in assuming that the expression a + b is coming in as run-time data, such as a parsed string from a user interface? If so, then you probably need to call eval on something because there’s no other way to cause a function to come into existence at run-time.

A related question is: are the symbols inside the expression always supposed to be fields of the struct? If so, can you just transform every symbol a into foo.a? In that case, you don’t need to know the field types of anything. I suspect that’s probably too simple for your real application, but it’s worth considering.

3 Likes

But it involves a DataType, not a global variable, right?

I’m not sure I understand the distinction you’re making. In any case, it’s easy to find examples that break it:

julia> macro doit(T)
           fnames = fieldnames(eval(T))
           # better:
           # fieldnames(Core.eval(__module__, T))
           @show fnames
       end
@doit (macro with 1 method)

julia> function foo(T)
         @doit(T)
       end
ERROR: LoadError: UndefVarError: T not defined

Note the error occurs before we even try to call foo.

Yeah, I get that. It is not possible to define that function at all. But the example should be something like:

julia> macro doit(T)
         fnames = fieldnames(eval(T))
         # better:
         # fieldnames(Core.eval(__module__, T))
         @show fnames
       end
@doit (macro with 1 method)

julia> struct bar
         a
         b
       end

julia> function foo(x::bar)
         @doit(bar)
       end
fnames = (:a, :b)
foo (generic function with 1 method)

julia> foo(bar(1,2))
(:a, :b)

Anyway, I get your concern (the code above is not the best). Let me first figure out which path I decide to follow to build my macro. Once I get that, I will post it. I have collected many useful ideas to process.