# Parametric Types with Union

**URL:** <https://discourse.julialang.org/t/parametric-types-with-union/121507>\
**Category:** New to Julia\
**Tags:** parametric-types, union\
**Created:** [October 20, 2024, 3:46pm UTC](https://discourse.julialang.org/t/parametric-types-with-union/121507 "2024-10-20T15:46:20Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![danrib07](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danrib07/32/212751_2.png) [@danrib07](https://discourse.julialang.org/u/danrib07)\
**Post date:** [October 20, 2024, 3:46pm UTC](https://discourse.julialang.org/t/parametric-types-with-union/121507/1 "2024-10-20T15:46:20Z")

</div>

I have the following snippet:

```julia
abstract type AbstractReaction end
abstract type AbstractSpecies end
abstract type AbstractRateLaw end

struct Species <: AbstractSpecies
    name::Union{AbstractString,Symbol}
end

struct MassAction{T<:Real,S<:Integer} <: AbstractRateLaw
    reactants::Union{Array{Tuple{AbstractSpecies,T},2},Dict{AbstractSpecies, T}}
    rate_constant::T
    T_dependence::S
end

```

The idea here is that the user can define some reactants and then use those reactants to construct a mass-action rate law. I am trying to make the field `MassAction.reactants` general, so that the user can pass two different types to instantiate the struct. For example, let’s say I define some reactants and some parameters that should be use to construct the `MassAction` rate law:

```julia
a = Species("A")
b = Species("B")

params_vec_float = [(a,1.0) (b,2.0)]
params_vec_int = [(a,1) (b,2)]
params_dict_float = Dict(a=>1.0, b=>2.0)
params_dict_int = Dict(a=>1, b=>2)

rate_law1 = MassAction{Float64,Bool}(params_vec_float,1.0,true)

```

Which leads to the following error:

```julia
ERROR: MethodError: Cannot `convert` an object of type 
  Matrix{Tuple{Species, Float64}} to an object of type 
  Union{Dict{AbstractSpecies, Float64}, Matrix{Tuple{AbstractSpecies, Float64}}}
Closest candidates are:
  convert(::Type{T}, ::T) where T at Base.jl:61
Stacktrace:
 [1] MassAction{Float64, Bool}(reactants::Matrix{Tuple{Species, Float64}}, rate_constant::Float64, T_dependence::Bool)
   @ Main ~/dev/julia/ReactorSimulator.jl/src/kinetics.jl:11
 [2] top-level scope
   @ REPL[7]:1

```

Upon reading the documentation, I found out that two objects the equivalency I am trying to use does not work because of [parametric type invariance](https://docs.julialang.org/en/v1/manual/types/#Parametric-Types)

My question is: how can I modify the type definition of the field `MassAction.reactants` so that any of the `params_vec_float, params_vec_int, params_dict_float, params_dict_int` would work when trying to instantiate the struct `MassAction`?

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [October 20, 2024, 8:23pm UTC](https://discourse.julialang.org/t/parametric-types-with-union/121507/2 "2024-10-20T20:23:46Z")

</div>

> [@danrib07](#):
>
> Upon reading the documentation, I found out that two objects the equivalency I am trying to use does not work because of [parametric type invariance](https://docs.julialang.org/en/v1/manual/types/#Parametric-Types)

The _type parameters_ are invariant, the actual type will subtype just fine e.g. `Point{Int} <: AbstractPoint{Int}`.

> [@danrib07](#):
>
> ```julia
> ERROR: MethodError: Cannot `convert` an object of type 
> Matrix{Tuple{Species, Float64}} to an object of type 
> Union{Dict{AbstractSpecies, Float64}, Matrix{Tuple{AbstractSpecies, Float64}}}
> 
> ```

Your use of union types is correct, so let’s ignore the `Dict` for now, as the type conversion is intended for the `Matrix`. Unlike most types, `Tuple`s are specially limited and thus allowed to have type parameter covariance, so `Tuple{Species, Float64} <: Tuple{AbstractSpecies, Float64}`. And as you note, the error is that subtyping of type parameters don’t extend to subtyping of the types, including `Matrix`. There are a couple ways to go here:

1. Full flexibility of inputs, minimal compilation of methods for them. Your matrix is currently _automatically_ determining the element type parameter `Species` because all the test inputs are `Species`. However, you might throw in a `(SpeciesX("newA"), 3.0)` in practice, in which case the element type will change. You could let base Julia try to figure out `AbstractSpecies` from the inputs, but if you know that’s the constraint, you can and should specify it manually `Tuple{AbstractSpecies, Float64}[(a,1.0) (b,2.0)]`. Now the `MassAction` call will work.
2. More type constraints, more optimized compilation of methods for each concrete constraint. If you expect to often put one concrete `Species` into a collection, then it’s worth making a parameter for it:

```julia
julia> struct MassAction2{T<:Real,S<:Integer,R<:AbstractSpecies} <: AbstractRateLaw
           reactants::Union{Array{Tuple{R,T},2},Dict{R, T}}
           rate_constant::T
           T_dependence::S
       end

julia> MassAction2(params_vec_float,1.0,true)
MassAction2{Float64, Bool, Species}(Tuple{Species, Float64}[(Species("A"), 1.0) (Species("B"), 2.0)], 1.0, true)

```

Note that the type constructor call omitted the type parameters because the type definition implicitly made another method that determined the parameters from the inputs. This behavior is overridden if you define your own constructor methods. If you specify `R` as `AbstractSpecies` manually, you get an unequal substitute to `MassAction`:

```julia
julia> dump(MassAction2{Float64, Bool, AbstractSpecies})
MassAction2{Float64, Bool, AbstractSpecies} <: AbstractRateLaw
  reactants::Union{Dict{AbstractSpecies, Float64}, Matrix{Tuple{AbstractSpecies, Float64}}}
  rate_constant::Float64
  T_dependence::Bool

julia> dump(MassAction{Float64, Bool})
MassAction{Float64, Bool} <: AbstractRateLaw
  reactants::Union{Dict{AbstractSpecies, Float64}, Matrix{Tuple{AbstractSpecies, Float64}}}
  rate_constant::Float64
  T_dependence::Bool

julia> MassAction2{Float64, Bool, AbstractSpecies} == MassAction{Float64, Bool}
false

```

As you can see, the `Union` is still in there and its still an abstract type, but it’s small enough that the compiler can optimize methods in many cases. Still, optimization is always more feasible when type parameters can narrow down fields to concrete types, so it’s worth considering refactoring. Food for thought, you want to be able to pass matrices and dictionaries as inputs, but do you really need to store them in the composite instance directly, e.g. you intend to mutate the inputs and the composite instance should adjust accordingly? Or can you process the inputs to a consistent format, whether you allow mutation of the composite instance or not? Similar input vs storage consideration applies to the `Union` in `Species`.

PS `Union{}` is itself a special abstract type, the ultimate subtype with no instances, so I amended the title.

---

<div class="post-metadata">

**Author:** ![danrib07](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danrib07/32/212751_2.png) [@danrib07](https://discourse.julialang.org/u/danrib07)\
**Post date:** [October 21, 2024, 1:42pm UTC](https://discourse.julialang.org/t/parametric-types-with-union/121507/3 "2024-10-21T13:42:45Z")

</div>

Thank you very much for this concise explanation!
