I’ve been messing around with the idea of streamlining dynamic dispatch on values for non hot path / glue code. The pattern is for multiple dispatch to dispatch to a function I define with a macro based on the known type information, and then the macro generated function dynamically dispatches based on conditional expressions but with syntactic sugar. For many of the cases I had in mind it doesn’t make sense to directly dispatch on value types as mentioned here Performance Tips · The Julia Language
A trivial toy example where an if statement would work just fine to demonstrate what I’m going for:
@dispatch_on_value epw(v::Vert) begin
label(v) == "C2" => inf_epw(v) # special case
true => sup_epw(v) # default / else case
end
Here is the macro I put together
macro dispatch_on_value(f, block)
@assert f.head == :call
@assert block.head == :block
cnd_pairs = Vector{Union{Pair{Expr},Pair{Bool,Expr},Pair{QuoteNode,Expr}}}()
for e in block.args
if isa(e, Expr) && e.args[1] == :(=>)
lhs = e.args[2]
rhs = e.args[3]
push!(cnd_pairs, lhs => Expr(:return, rhs))
end
end
expr::Union{Nothing,Expr} = nothing
for i = lastindex(cnd_pairs):-1:firstindex(cnd_pairs)
lhs = first(cnd_pairs[i])
rhs = last(cnd_pairs[i])
if i > firstindex(cnd_pairs) && isnothing(expr)
if isa(expr, Bool)
@assert expr
expr = Expr(:else, rhs)
else
expr = Expr(:elseif, lhs, rhs)
end
elseif i > 1
expr = Expr(:elseif, lhs, rhs, expr)
else
expr = Expr(:if, lhs, rhs, expr)
end
end
fn = Expr(:function, f, expr)
eval(quote
$fn
end)
end
Are there any potential footguns with this type of approach in general? I was thinking it should be possible for the macro to automatically evaluate what approach it would use internally, ie a hash table where possible and otherwise a switch/if/elseif/else based approach. Not that performance matters that much here, but why not have my cake and eat it too?
I don’t quite understand the usecase of this macro tbh. I think it is much clearer to just write out the function normally:
function epw(v::Vert)
label(v) == "C2" && return inf_epw(v) # special case
return sup_epw(v) # default / else case
end
Maybe you could make an example where your macro really shines?
Also using eval in a macro is almost never what you want. I think you might be doing it here to circumvent the macro hygiene. You should use esc for that. I haven’t tried it but this should work as well:
To save some clicks, this is roughly what it expands to:
(Main.M1) julia> @macroexpand @dispatch_on_value_noeval epw(v::Vert) begin
label(v) == "C2" => inf_epw(v) # special case
true => sup_epw(v) # default / else case
end
:(function epw(v::Vert)
if label(v) == "C2"
return inf_epw(v)
elseif true
return sup_epw(v)
end
end)
I hesitate to call this multiple dispatch, though I think it could be in a generalized sense, because it isn’t the argument type-based and possibly static kind of multiple dispatch that Julia does. The syntax seems more like pattern matching, implemented by an if statement here. Looks like the value-matching of Match.jl to me.
I did a poor job explaining the actual use case, but this is an exercise in exploring meta programming as a way to automatically output optimal Julia code depending on the inputs.
For instance say you want to generate a function that selected a function based on several values. A macro could go through all of the expressions, see which values are checked, and then check it the expression could map to a vector or dictionary lookup for the specific arguments and their values or if more complex conditionals are still needed. So say you had 3 boolean variables you wanted to dispatch on, you have 8 potential combinations. You could do a bunch of nested if statements, but it might be faster to just dispatch on Vector{Func} (ideally a constant version of Vector so the compiler can still optimize) mapping the booleans to an integer index in [1, 8]. If the variables included numbers or strings on the other hand its likely not practical to use such an approach, a hash table or conditionals would be better. So the idea is to try and recognize these patterns and then output the most performant version of the dispatch possible while providing syntactic sugar / cleaner looking code.