Writing this inner constructor with a macro

Hello,

I am trying to write a macro to reduce some boilerplate. I have some builder method for structs to do some complicated transformations on a dict. I am copypasting something similar to this over and over:

struct Vehicle
    engine::Engine
    brand::String

    function Vehicle(params::AbstractDict)
        args = build(Vehicle, params)
        if  typeof(params) <: Exception
            args
        else
            new(args...)
        end
    end
end

and I would like to just do it like this:

struct Vehicle
    engine::Engine
    brand::String

   @build(Vehicle, params)
end

I prefer this to wrapping the whole struct on a macro (seems less explicit and well, I am not even able to write even this simple macro, so…).

I am trying to build this simpler example

macro build(type, params)
    quote
        function $(esc(type))($(esc(params))::AbstractDict)
            args = build($(esc(type)), $(esc(params)))
            $(esc(Expr(:call, :new, args...)))
        end
    end
end

This is the only way I found to call new. But then of course args is undefined.

Any suggestions or something obvious I am missing?

The only thing which needs escaping here is type, since that must match the name of the struct. The others, params and args, are local variables whose names don’t matter; not escaping them means that they cannot clash with e.g. type.

The other thing is that you have an args (which isn’t defined) where you want :args. Often it helps to dump what you’re trying to construct:

julia> :(new(args...)) |> dump
Expr
  head: Symbol call
  args: Array{Any}((2,))
    1: Symbol new
    2: Expr
      head: Symbol ...
      args: Array{Any}((1,))
        1: Symbol args

julia> Expr(:call, :new, Expr(:(...), :args))
:(new(args...))

julia> macro build(type)
           quote
               function $(esc(type))(params::AbstractDict)
                   args = build($(esc(type)), params)
                   $(Expr(:call, :new, Expr(:(...), :args)))
               end
           end
       end
@build (macro with 1 method)

julia> @macroexpand @build Vehicle
quote
    #= REPL[26]:3 =#
    function Vehicle(var"#30#params"::Main.AbstractDict)
        #= REPL[26]:3 =#
        #= REPL[26]:4 =#
        var"#29#args" = Main.build(Vehicle, var"#30#params")
        #= REPL[26]:5 =#
        Main.new(var"#29#args"...)
    end
end

Whether this is a good idea, rather than say making a parametric type, is another question of course.

1 Like

Thanks a lot for your answer. Unfortunately, that doesn’t seem to work when used as I intend to:

struct Example
    stuff::String
    @build(Example, params)
end

julia> Example(Dict(:stuff => "thing"))
# UndefVarError: new not defined

Whether this is a good idea, rather than say making a parametric type, is another question of course.

Well, I am already doing it (with I think good reason), just copypasting the boilerplate constructor over and over is what I want to abstract away. Not sure how a parametric type would help.

EDIT: Simply escaping new works. Again, thanks a lot for your help :slight_smile:
I think it’s because new() can only be called from inside a struct, and therefore needs to be escaped to be in the right context.

macro build(type, params)
    quote
        function $(esc(type))(params::AbstractDict)
            args = build($(esc(type)), params)
            $(Expr(:call, esc(:new), Expr(:(...), :args)))
        end
    end
end