I’m trying to create a macro that will help me build structs from a schema defined in a JSON document and while I have something that works, I would love a code review so I can learn how to do it better…
macro make_struct(struct_name, schema)
fields=[:($(Symbol(entry[1])) :: $(Symbol(titlecase(entry[2])))) for entry in eval(schema)]
return Expr(:type, false, eval(struct_name), Expr(:block, fields...))
end
macro make_struct(name, schema)
fields=[:($(Symbol(entry[1]))::$(Symbol(titlecase(entry[2])))) for entry in eval(schema)]
:(struct $(esc(name))
$(map(esc,fields)...)
end)
end
@make_struct STRUCT_NAME [["x","int32"]]
I’d say the thing you missed was adding the newline immediately after the struct name. It needs this for some reason.
Also you should probably escape the name of the struct and it’s fields otherwise the structs will be defined in the module the macro is defined in and it will look for DataType’s used in the fields there too.
The vanilla way of defining your macro is like this:
macro make_struct(struct_name, schema...)
fields=[:($(entry.args[1])::$(entry.args[2])) for entry in schema]
esc(quote struct $struct_name
$(fields...)
end
end)
end
println(@macroexpand @make_struct STRUCT_NAME (x,int32) (y, float32))
@make_struct STRUCT_NAME (x, int32) (y, float32)
But my usage of eval and building my expression with the Expr(…) syntax feels like I’m missing a key point.
You should essentially never call eval inside a Macro. A macro is a source code transformation. It accepts code (Expr/Symbol objects), and outputs code, which will take the place of the macro invocation.
The Julia section on metaprogramming is pretty good. There are also beginner-level tutorials on Lisp macros on the web.
Once you’re more comfortable with macros, have a look at MacroTools. It makes writing them a lot easier.
Thanks. I think the part I’m missing is how to go from my array of strings into the evaluated tuple of symbols.
My use case is I have a schema file S1.json on disk with the following structure [["x","int32"],["y","float32"]] and I’m trying to evaluate this into a struct so I can slurp in an array of these structs with open(f->read(f, S1, Int64(stat(filepath).size/sizeof(S1))), filepath, "r")
If I type out the fields in the structure, then this macro works just fine
function parse_schema(schema)
function schema_helper(element)
:($(Symbol(element[1]))::$(Symbol(titlecase(element[2]))))
end
tuple(map(schema_helper, schema)...)
end
z1=Any[Any["x","int32"],Any["y","float32"]] # from a JSON.parse(...)
println(parse_schema(z1))
macro make_struct(struct_name, schema...)
esc(
quote
struct $(struct_name)
$(schema...)
end
end
)
end
println(@macroexpand @make_struct S1 parse_schema(z1))
I just get the following
begin
struct S1
parse_schema(z1)
end
end
I understand that this is just parsing my expression and making a new one. Every time I think about how I would put a for loop on the inside of the struct, I come to the solution i should be writing a function that builds an Expr and then just evaling the new expression in my repl. Should I be avoiding a macro all together?
Yeah, I think so. You can eval your structs into existence while parsing the file. You might run into world-age issues, though, but they are solvable with more eval.
Also consider that you can essentially define new data types at runtime with tuples and parametric types, without eval:
> struct JSONStruct{NAME, T<:Tuple}
fields::T
JSONStruct{NAME}(fields...) where NAME = new{NAME, typeof(fields)}(fields)
end
> JSONStruct{:apple}(1,2,"hey")
JSONStruct{:apple,Tuple{Int64,Int64,String}}((1, 2, "hey"))
> Apple = JSONStruct{:apple,Tuple{Int64,Int64,String}}
but you need to be very comfortable with parametric types, and it can get very messy. See eg. TypedTables.jl
Well, since the thread is titled “Trouble understanding macros…”, let’s use @cstjean’s first pattern:
module M
using JSON
export make_schema_struct
macro make_struct(struct_name, schema...)
fields = [:($(x[1])::$(x[2])) for x in schema...]
esc(quote struct $struct_name
$(fields...)
end
end)
end
function make_schema_struct(datadir,dataset::String)
schema = open(JSON.parse, joinpath(datadir,"$dataset/schema.json"), "r")
schema = [(Symbol(x),Symbol(titlecase(y))) for (x,y) in schema]
eval(current_module(),:(@M.make_struct $(Symbol(dataset)) $schema))
end
end
using M
make_schema_struct("/path/to/data","MyDataset")
Pretty much the same as yours, but a bit less expression-wrangling.