The Unreasonable Efficiency and Effectiveness of Multiple Dispatch: Your Favourite Examples

Personally, I like naming my functions after the thing it calculates, e.g.

voltage(i::Number, r::Number) = i * r
current(v::Number, , r::Number) = v / r
resistance(v::Number, i::Number) = v / i

The next layer of challenge I experience in general is the fact that there’s many ways to compute these terms. At first I tried dispatching off Vals, e.g. (with power)

voltage(::Val{:IR}, i::Number, r::Number) = i * r
voltage(::Val{:PI}, p::Number, i::Number) = p / i
voltage(::Val{:PR}, p::Number, r::Number) = sqrt(p * r)

I thereafter realised that users can’t extend my interfaces/models without piracy. And in my field, there are A LOT of metrics and A LOT of models for computing those metrics, and those models can use the same parameters which have the same units, which throws the dispatching by Unitful.jl quantities out the window :sob:. (See Speed of sound in sea water for an example of what I’m talking about, and that’s just for ocean sound speed; it’s only a small subset of equations for sound speed; there’s many metrics in ocean acoustics that also have a variety of models). So the number of models and functions I need to define starts to proliferate in my field of ocean sonar. (For those interested in why, it’s because the theoretical models have proven insufficient in modelling reality — we need real-world-data-interpolated models, and there’ve been so many papers on developing equations based on recorded data for ocean sound speed alone. The same is true for bottom reflection, acoustical scattering, ambient noise, … this list of things is very long.)

Defining a new type ala Voltage, Resistance, Current above will very quickly start overpopulating and overpolluting the namespace. Additionally, while “Sound Speed” would be a desired output metric — so there’d be a function named sound_speed, there’ll also be other metrics that will be best labelled by a model named “Sound Speed” so I’d have SoundSpeed as a type. Again, this leads to namespace overpopulation and overpollution. Plus the inconsistency of having SoundSpeed as a type for numeric dispatch, and SoundSpeed as a type for model dispatch, just clashes.

I’ve started exploring having one abstract type, abstract type ModelName end which my users and I can MyModelName <: ModelName and then extend sound_speed and other functions by the new model.

Another layer of complexity is that in my field of ocean sonar, sound_speed also needs to be calculated for the seabed (and atmosphere), so I’ve tried out having sound_speed(::Seabed, ::Clay, z::Real) with sound_speed(::Ocean, ::Mackenzie, z::Real)

OR

ocean_sound_speed(::Mackenzie, z::Real) with seabed_sound_speed(::Clay, z::Real).

OR (and I gave up on this one very early because nested module navigation and namespace management between the layers was a bit of a nightmare in Julia, which I assume/hope will get easier in the future)

module OceanSonar
  abstract type ModelName end

  struct Mackenzie <: ModelName end
  struct Clay <: ModelName end

  module Ocean
    using ..OceanSonar: Mackenzie

    sound_speed(::Mackenzie, z::Real) = ...
  end

  module Seabed
    using ..OceanSonar: Clay

    sound_speed(::Clay, z::Real) = ...
  end
end

which will require e.g. import OceanSonar.Ocean: sound_speed for extension.
I think the latter needs to be my solution. The top level OceanSonar will contain all the <:ModelName subtypes for dispatching, and then the inner modules will define methods on them.

Finally, my users (and I) will DEFINITELY want to query what models are available for what function, and what inputs are needed for each model. Maybe traits? Either traits or introspection with things like InteractiveUtils.jl and MethodInspector.jl. I think I’ll have to go with traits, but this is the the layer of exploration I’m up to right now, and I’ve explored the latter plenty and the former a little but currently. On top of that, as per Julia’s progress in the generation of binaries, I may have troubles with introspection moving ahead.

I can’t find much in the Julia ecosystem that explores this. As far as I’ve seen, they resort to defining new types for each model. As argued above, I arguably can’t do that.

So to bring this thread full circle, the above complexity is most easily simplified and implemented in Julia with its multiple dispatch paradigm. I’ve been trying it out in various languages (i.e. MATLAB and C++) and OH BOY I definitely, definitely want to use Julia for this. You should see my boss’ code for trying to do this model and input variable dispatching in MATLAB. So. Much. Boilerplate. Even more boilerplate in C++ where it’s a whole lot of nested conditionals and doesn’t have introspection. I still have plans on exploring how other languages would handle this, but the fact that Julia is the only language that has multiple dispatch as the main paradigm makes me somewhat content for the time being.

Bringing this comment full circle, I also have plans on exploring acausal modelling both in differential equations (which applies to the field of ocean acoustics as a subfield of ocean sonar) and in just the equation above. The implementations of the VIR relationship are just one equation, so in the spirit of acausal modelling it should be theoretically possible to define the equation V = IR and then somehow specify the inputs and desired output, obviating the boilerplate of writing a function for every manipulated version of the equation. This is possible with the recent advances of Symbolics.jl in symbolic equation solving.

Additional Thoughts: I’ve also been thinking a lot (long before this thread), how best to do something like how packages can write code for something that is Tables.jl compliant, i.e. it will accept types defined off Tables.jl but it will be completely unaware of what that type is, and it will “just work.” For example, the ocean acoustics submodule of OceanSonar will have a variety of ways to solve the wave equation (Helmholtz equation, Eikonal equation, etc.) and various solvers from DifferentialEquations.jl should be accessible by my package, without my package knowing what that solver is. There’s a few other contexts where such a composable design pattern is needed. Julia enables such composable design to increase linearly with the number of types, with a multiplicatively exploding number of functionalities. In half-theory at least (half because we’ve already demonstrated this in so many places, a recent magical moment I had was seeing how DimensionalData.jl works so well with AlgebraOfGraphics.jl, and they don’t even know each other, but Tables.jl facilitated their composability — the other half is, I’m still writing my package so we’ll see how I go).

3 Likes