I’m trying to build a chemical process modelling library that takes live sensor data as input, and one of the perennial challenges to this is the fact that there are many different types of flow meters with multiple instruments having different unit dimensions. Currently, I’m looking at four different types:
Mass flow meter (coreolis) measuring mass flow directly
Volume flow meter with densitometer, measuring volumetric flow and mass flow
Volume flow meter with thermodynamic state (includes volumetric flow, pressure, temperature, and composition)
Differential pressure flow meter (includes two pressure measurements, temperature and composition)
Now anywhere a flow meter exists, it could be one of these four types (I may even run into more types in the future). Their APIs will all look extremely similar, as I’m calculating mass flows with all of them. I would like this to be type-stable due to some low-level operations, but I don’t want to have type parameters for every arbitrary stream. This would look like a good candidate for a TypeUnion, but large type unions are discouraged, and there seems to be no universal agreement as to whether four types is below the limit or not. The other approach could be to build out another struct with double-type unions like
struct GenericFlowMeter
mass :: Union{MassFlowMeter, Nothing}
voldens :: Union{VolDensMeter, Nothing}
volume :: Union{VolFlowMeter, Nothing}
pressdiff :: Union{DiffPressMeter, Nothing}
end
and then use if-statements on core API functions to select the method based on the first non-Nothing element.
I think ManualDispatch.jl was meant to solve this problem, but it seems unmaintained. I’m not sure where the Julia community has landed on with respect to this problem and I couldn’t find anything about it in Kwong’s Hands On Design Patterns book (which only gives 2-type union examples). The SciML guideline strongly discourages TypeUnions with more than 2 types, while the Julia documentation simply states in a footnote that the default union splitting limit is 4. This means my problem might be getting a bit too big for union splitting and I’m not sure if my other solution above would be feasible.
abstract type GenericFlowMeter end
struct MassFlowMeter <: GenericFlowMeter end
etc not meet your needs? You can use dispatch to write specialized API where necessary while writing functions for general GenericFlowMeters where possible.
I actually have an AbstractFlowMeter already. The problem is, if I have a tank with an arbitrary set of flow meters on the inlet and outlets, I have a container with a potentially abstract type:
struct Tank
inlets :: Vector{AbstractFlowMeter}
outlets :: Vector{AbstractFlowMeter}
end
Having a container with abstract types would cause dynamic dispatch, reducing performance and likely breaking JuliaC if I wanted to make a small binary. Using a type union instead of an abstract type would eliminate this, but if the type union is too large, it will default to dynamic dispatch.
Unfortunately, a tank can have an arbitrary number of inlet/outlet flows, so a tuple wouldn’t work. Moreover, the tank itself is part of another system so this would still cause issues for overspecialization. WrappedUnions.jl looks like it’s trying to solve my exact problem; it seems like an improved version of LightSumTypes.jl according to someone who wrote/contributed to both packages. Thank you so much for suggesting this, I’ll give this one a try!
FWIW it’s very short, appears to use only public functions, and restricts its 2 dependencies to known versions, so it would hypothetically work forever. The one downside is it restricts MacroTools, so it starts becoming obsolete in the unlikely event that gets an update. WrappedUnions has the same approach of type-branched duplicate calls, only has Julia v1 as a dependency, and is based on a generated function that doesn’t need you to write out types like ManualDispatch. WrappedUnions does use an internal Base function, but it’s an easy edit in case that ever changes.
I tend to end up with this pattern a lot, too. I’ve found that, for my purposes, the cost of the dynamic dispatch isn’t so bad. What really hurts performance is that inference can’t tell what the return type is when you call functions that refer to those fields. If you can assert the return type, you might keep the flexibility, but gain back some performance.