Multiple dispatch "default case"?

I’m asking because I think I am probably going about this the wrong way. I have a “configuration file” with sections that I wanted to parse and store in a dictionary. I’m reading the sections like this:

function read_section(file_handle)
    name = String(read(file_handle, 32))
    s_len = ntoh(read(file_handle, UInt32))
    data = read(file_handle, s_len)

    parse_section(Val(Symbol(name)), IOBuffer(data))
end

The section parsers look like this:

function parse_section(::Val{:type1}, data)
    # ... do stuff
end

function parse_section(::Val{:type2}, data)
    # ... do different stuff
end

However when I get a section name that isn’t coded as a parse_section(::Val{:something_i_didnt_code}, data) … I get an error. I could use a try/catch block … or … perhaps I am just doing things wrong?

That’d be parse_section(::Val, data) = nothing. But I’d also not use dispatch for this, and instead use a sequence of ifs or the like. But really, I’d try to use a standard file format (TOML or the like) if that’s at all a possibility.

2 Likes

I mean you can just add a default method that is unconstrained, f.e. parse_section(x, data) = error("Unknown section: $x") or maybe like parse_section(::Val{T}, data) where T = error("Unknown section: $T")

2 Likes

I started out with a sequence of if-s but wanted to explore the modularity of having separate functions. I guess with the if-s that the else block would handle the missing case.

Is there a bad performance penalty for using the parse_section(::Val{}, data) overloads? I guess I could code up the same with an if block and try it out myself …

I’m willing to rewrite it all, but I wanted to try out some different ways so that I could get some hands-on experience. I find that I often don’t learn well from documentation - I have to code up something and evaluate it “live” so to speak.

1 Like

Quick benchmark:

using BenchmarkTools
f(x) = f(Val(x)) # convenience method
f(::Val) = return 0 # default
f(::Val{:x}) = return 1
f(::Val{:y}) = return 2
f(::Val{:z}) = return 3

function g(val)
    if val == :x
        return 1
    elseif val == :y
        return 2
    elseif val == :z
        return 3
    else
        return 0
    end
end

testvec = [:x, :y, :z, :y, :x, :z]

julia> @btime f.($testvec)
  1.956 μs (8 allocations: 400 bytes)
julia> @btime f.($(Val.(testvec)))
  429.236 ns (1 allocation: 128 bytes)
julia> @btime g.($testvec)
  38.903 ns (1 allocation: 128 bytes)

So without using the convenience wrapper you are an order of magnitude slower. When also considering the overhead of wrapping the value in Val you are more like 50x slower. Here Julia can’t infer which function to dispatch to so the allocation of the Val wrapper cannot be avoided. Whether that timing difference matters in your usecase is for you to decide.

1 Like

Your functions just return a value. Their functions will operate over a file handle (basically the slowest primitive thing you can do in any language). I think this is not really representative. What you are benchmarking is just the overhead of multiple dispatch, so “how many times slower” is not relevant, but if the absolute difference is significant in relation to the real cost of each of their functions.

2 Likes

That’s what GitHub - ztangent/ValSplit.jl: Compile away dynamic dispatch on Val-typed arguments via value-splitting. was designed for. I haven’t read this thread in enough detail to say if it’d make sense over just using an if-else chain, but it exists.

I posted about this in the #appreciation channel on slack about this pattern a few weeks ago, because to me, it feels really elegant

BUT i got schooled (very nicely) by Tim Holy and Claire Foster about the problem - yes, there’s a big performance penalty (or at least there can be, I think). If you have a slack account, you can read their points here, and I’ll quote below (with permission) so it doesn’t get lost on the slack hole. For context, I had made a function called fixcol() with different methods dispatching on Val types based on the name of the column, very similar to what you have.

In response to me saying it was still fast enough for my purposes:

Then later, from Claire:

2 Likes

Thanks for re-posting that! And thanks also to Tim & Claire for comments on your question!
I did find the dispatch-on-Val fun to try out, and I’m pretty sure in my use-case it is not a problem - but in general I like to take the route of efficiency. I think for “real” work, based on the input here, I would either use if-else or ValSplit.

I’m sure like a lot of people here, that neatness of Julia doesn’t really become apparent until you try to code something nontrivial and new.

3 Likes