I’m working on an implementation of the PBRT 4 raytracer in Julia. To avoid having containers of abstract types for primitives, materials, etc, some of the base object types are defined using a tag, instead of subclasses, for example
@enum Shape SPHERE CUBE
struct Primitive
shape::Shape
center::SVector{3, Float64}
size::Float64 # radius or side-length
end
I want to be able to dispatch on the value of the tag. The simplest way I can see is doing this. All “sub-functions” return the same type, which is easily inferable.
intersect_sphere(p::Primitive, r::Ray) = ...
intersect_cube(p::Primitive, r::Ray) = ...
function intersect(p::Primitive, r::Ray)
p.shape == SPHERE && return intersect_sphere(p, r)
p.shape == CUBE && return intersect_cube(p, r)
end
Since this is something that I will use a lot, I tried writing a macro to automate this pattern. This also avoids forgetting one of the enum values in the dispatch. However, I have the issue that the macro needs to know about Shape
, specifically its instances, to generate this code. This is my current implementation:
macro tagdispatch(enum, f, sig, tag=:tag)
fname = String(f)
argsymbs = [gensym(string(T)) for T in sig.args]
headerexpr = Expr(:call, f,
[
Expr(:(::), s, T) for (s, T) in zip(argsymbs, sig.args)
]...
)
blockexpr = Expr(:block,
[
:($(argsymbs[1]).$tag == $v &&
return $(Symbol(fname * "_" * lowercase(string(v))))($(argsymbs...)))
for v = instances(eval(enum))
]...
)
esc(Expr(:function,
headerexpr,
blockexpr
))
end
@tagdispatch Shape intersect (Primitive, Ray) shape
This works, but to get access to the enum from its symbol, I use eval(enum)
, which I know is generally bad in macros. Specifically, what are the dangers of doing this, considering that in my case the enum will always be defined before the macro is used.
If it is definitely something to avoid, is there another way to solve this, other than write the functions by hand (which I could do, if necessary)?