Value Dispatch Macro

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:

return esc(fn)
2 Likes

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.

2 Likes

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.

I am not entirely sure as I do not fully understand your usecase, but 2 interesting packages in that area might be ValSplit.jl and Virtual.jl.

1 Like