Hi there. I’m new to metaprogramming and I’m running into trouble when trying to create a macro for replacing some bolier plate code. The code creates a series of structs and their corresponding external constructors.
One example for the struct and the constructor is below:
struct FnaP <: FastaPaths
p::String
ext::String
end
function FnaP(p::String)::FnaP
ext = getFileExtention(p)
if ispresent(ext, CLM_General.ALLOWED_EXT["FnaP"])
return FnaP(p, ext)
else
error("The file '$p' is not a valid fasta nucleic acid file.")
end
end
My code above creates three methods for the FnaP constructor, which accept different types of inputs:
Because I need to declare maye similar structs and corresponding constructors, I’ve created a macro as follows:
macro definePathDT(name::Symbol)#, allowed_ext::String)
quote
struct $name <: PathsDT
p::String
ext::String
end
function $name(p::String)
ext = getFileExtention(p)
if ispresent(ext, ALLOWED_EXT[$(string(name))]) #CLM_General.
return $name(p, ext)
else
error("The file '\$p' is not a valid $(string(name)) file.")
end
end
end
end
When I call the macro with
@definePathDT FnaP
I only get two methods for my FnaP constructor, both corresponding to the inner constructor:
@macroexpand is very helpful in debugging macro code. Invoking your macro yields
julia> @macroexpand @definePathDT FnaP
quote
#= REPL[1]:3 =#
struct FnaP <: Main.PathsDT
#= REPL[1]:4 =#
p::Main.String
#= REPL[1]:5 =#
ext::Main.String
end
#= REPL[1]:8 =#
function var"#41#FnaP"(var"#43#p"::Main.String)
#= REPL[1]:8 =#
#= REPL[1]:9 =#
var"#42#ext" = Main.getFileExtention(var"#43#p")
#= REPL[1]:10 =#
if Main.ispresent(var"#42#ext", Main.ALLOWED_EXT["FnaP"])
#= REPL[1]:11 =#
return var"#41#FnaP"(var"#43#p", var"#42#ext")
else
#= REPL[1]:13 =#
Main.error("The file '\$p' is not a valid $(Main.string(Main.name)) file.")
end
end
end
Look at the mangled variable names. The macro hygiene pass has converted all function and variable names into generic ones to avoid clashing with existing ones, e.g. a function $name might already exist in the evaluating module.
This shouldn’t pose a problem here.
To prevent macro hygiene, enclose the returned expression with esc(...).
macro definePathDT(name::Symbol)#, allowed_ext::String)
esc(quote
struct $name <: PathsDT
p::String
ext::String
end
function $name(p::String)
ext = getFileExtention(p)
if ispresent(ext, ALLOWED_EXT[$(string(name))]) #CLM_General.
return $name(p, ext)
else
error("The file '\$p' is not a valid $(string(name)) file.")
end
end
end)
end
julia> abstract type PathsDT end
julia> @definePathDT FnaP
FnaP
julia> methods(FnaP)
# 3 methods for type constructor:
[1] FnaP(p::String)
@ REPL[6]:8
[2] FnaP(p::String, ext::String)
@ REPL[6]:4
[3] FnaP(p, ext)
@ REPL[6]:4
This is what the escaped expression looks like. The other effect of escaping besides changing some variable names (the docs call them “local” to the expression but the criteria isn’t very clear there) is that function calls are not taken from the macro definition’s scope anymore; the Main. are gone so the symbols will instead take from the macro call scope. This is as close to pasting code as it gets, and if some symbols are intended to belong to the macro definition scope, it’s just a matter of writing multiple esc at the other subexpressions (there is no unescaping).
julia> @macroexpand @definePathDT FnaP # the esc version
quote
#= REPL[16]:3 =#
struct FnaP <: PathsDT
#= REPL[16]:4 =#
p::String
#= REPL[16]:5 =#
ext::String
end
#= REPL[16]:8 =#
function FnaP(p::String)
#= REPL[16]:8 =#
#= REPL[16]:9 =#
ext = getFileExtention(p)
#= REPL[16]:10 =#
if ispresent(ext, ALLOWED_EXT["FnaP"])
#= REPL[16]:11 =#
return FnaP(p, ext)
else
#= REPL[16]:13 =#
error("The file '\$p' is not a valid $(string(name)) file.")
end
end
end
Thanks @skleinbo and @Benny. I’ve used the esc() and now it works as intended. I have a further question now.
How can I write a for loop to use the @definePathDT to generate multiple Data Types.
Macros are not like function calls that happen at runtime, they transform the code as Julia reads it, before it’s actually run. So what @definePathDT mynames[i] is working on isn’t Fna or Faa, it’s the expression mynames[i]. That error means that your definePathDT(::Symbol) macro method can’t work on ::Expressions, but even if you evade that error, mynames[i] makes no sense as an input e.g. struct mynames[i] <: PathsDT.
So, we need to construct an expression with a macro call at runtime and use a function call to evaluate it at runtime.
for i in eachindex(mynames) # fixed eachindex typo
eval( :( @definePathDT $(mynames[i]) ) )
end
Funnily enough there’s a macro to save writing out the quote delimiters, but it just expands to the runtime function call above. Bear in mind that @eval was designed to handle $ as if it were quote interpolation, $ is not generally something that works in macro expressions.
for i in eachindex(mynames) # fixed eachindex typo
@eval @definePathDT $(mynames[i])
end
I’m sure you can also put together that you didn’t need a macro at all, you could’ve evaled the big quote in a loop, where you won’t have to consider macro hygiene and variable mangling. This pattern is explained in the section Code Generation of the Metaprogramming page of the Manual.
@Benny yes you are right, I don’t need to create a macro for generating boiler plate code. It seems like I was using a very round about way to solve problem.
As you suggested, I used directly eval() function and my code looks like this now:
for sym in [:FnaP, :FaaP, :FastaQP, :BamP, :TableP]
eval(quote
struct $sym <: PathsDT
p::String
ext::String
end
function $sym(p::String)
ext = getFileExtention(p)
if ispresent(ext, ALLOWED_EXT[$(string(sym))]) #CLM_General.
return $sym(p, ext)
else
error("The file '\$p' with extention '\$ext' is not a valid $(string($sym)) file.")
end
end
end)
end
It works quite fine! The newly generated data types and constructors can be exported and used from other modules, like regular code.
Thank you both for helping me wrap up my head around metaprograming.