Dynamic union of parametric types

I am trying to define Union types that collect different instances of different types, it should be possible to append newer types that are defined later on, so that the union types can grow

here an example:

module Elements


CElems{C,M}   = Union{} where {C,M}
Elems2D{C,M}  = Union{} where {C,M}
Elems3D{C,M}  = Union{} where {C,M}


struct quad{C,M} end
struct tria{C,M} end
struct hexa{C,M} end
struct tetra{C,M} end


Elems2D{C,M} = Union{CElems{C,M}, quad{C,M}} where {C,M}
Elems2D{C,M} = Union{CElems{C,M}, tria{C,M}} where {C,M}

Elems3D{C,M} = Union{CElems{C,M}, hexa{C,M}, tetra{C,M}} where {C,M}

CElems{C,M}  = Union{Elems2D{C,M}, Elems3D{C,M}} where {C,M}


end

it gives me this error, on line 15 (Elems2D{C,M} = Union{CElems{C,M}, quad{C,M}} where {C,M})

ERROR: LoadError: TypeError: in Type{...} expression, expected UnionAll, got Type{Union{}}

things work if the types are not parametric, but I do need that

hope I have been clear about what I am trying to obtain

Sounds like what you actually want is abstract type?

This is nonsensical: Union{} where {C,M} is just the bottom type (Union{}), because the body of the UnionAll, Union{}, does not depend on either of the type parameters, C and M. See it in the REPL:

julia> Union{} where {C,M}
Union{}
 
julia> CElems{C,M}   = Union{} where {C,M}
Union{}

Personally, I don’t get it. In fact, this very much seems like an XY problem:

So, could you tell more about the what you’re trying to solve, instead of the attempted solution?

I thought about abstract type but I don’t know if it is possible to make quad both a subtype of CElems and Elems2D.

it appears to me that you can define a type as a subtype of an abstract type only at creation and concrete types can only have one parent abstract type or am I wrong?

I need to define types for different kind of elements in a Finite Element software, so that later on I can define functions that work on 2D elements, or 3D elements, or continuous elements (that can be either 2D or 3D), and so on with a rich landscapes of elements with different features that have different tags attached (hope it is not too much convoluted)

There is no multiple inheritance in Julia. So, no, a concrete type can’t subtype two different abstract types, except if one of the two abstract types already subtypes the other one.

You’re right on both counts.

Perhaps a good approach would be to give your types what is sometimes called “traits”, and then dispatch on the traits instead of on your types directly. Perhaps something like this:

struct Elems2D end
struct Elems3D end
struct CElems end

"""
    trait(x)

Return `Elems2D()` for two-dimensional elements, `Elems3D()` for three-dimensional elements or `CElems()` for continuous elements.
"""
function trait end

An example of where this pattern is used is Base.IteratorSize.

Looking at your original code again, it’s quite possible what you want could be achieved just using Union (and possibly UnionAll), however it’s still not clear to me what is the expected result. Could you sketch the desired subtyping relationship between the types you want defined?

For example:

  • from CElems{C,M} = Union{Elems2D{C,M}, Elems3D{C,M}} it seems like you want (Elems2D <: CElems) && (Elems3D <: CElems)

  • from Elems3D{C,M} = Union{CElems{C,M}, hexa{C,M}, tetra{C,M}} it seems like you want CElems <: Elems3D

The implication is Elems3D == CElems, and, similarly, Elems2D == CElems, which is surely not what you actually want.

correct there are other elements that are Elems3D but are not CElems, and so on

The Union works if I define all of the atomic types first and then define the unions, but I really want it to be dynamic

at the same time union can be dynamic (i.e. I can append new atomic types to existing unions) but on if the unions do not have parameters, which I need to correctly dispatch over the proper function

That’s not possible. Can you show what you mean?

this does not give errors

module Elements


CElems   = Union{}  
Elems2D  = Union{}  
Elems3D  = Union{}  


struct quad end
struct tria end
struct hexa end
struct tetra end


Elems2D = Union{CElems, quad}  
Elems2D = Union{CElems, tria}  

Elems3D = Union{CElems, hexa, tetra}  

CElems  = Union{Elems2D, Elems3D}  


end

and at the REPL after running it i get :

julia> Elements.CElems
Union{Main.Elements.hexa, Main.Elements.tetra, Main.Elements.tria}

that why I think it should be possible to implement it with parameters also

Your code example doesn’t “append new types to an existing union”. When you do this:

You’re defining three global variables. Next, when you do this:

… you’re not appending types to a preexisting Union (that’s not possible), you’re just reassigning to the global variable CElems, changing its value.

Does this help?

but these line seem to append types to existing union

Elems2D = Union{CElems, quad}  
Elems2D = Union{CElems, tria}  

the first time Elems2D only contains quad, the second time it contains quad and tria

you are right Elems2D in the end is only a variable, but that does get me what I want

I must be seeing thing from some wrong angle

To “append to existing union” would mean to mutate an existing value. Instead what these lines do is reassign different values to existing variables.

1 Like

In case it’s not clear, here I’m just asking you which of your types do you want to subtype which of your other types.

it should be dynamic, as soon as new types are defined I wand to be able to append it to all the groups it belongs,

for instance a new element can be:

  • “discrete” or “continuous”;
  • “1D”, “2D”, or “3D”
  • etc

more than subtypes I am try to tag types

1 Like

In that case I suggest you go with the “trait”-based approach I pointed to above. In your case discrete vs continuous would be one function, dimensionality would be another, etc. Then you add a method for this function when defining a new type.

1 Like

but I want to take advantage of the runtime multiple dispatch, so that I keep adding functions with the same name, and when I add new elements or functionality the same code will work with the old and the new elements

as far as I have understood the traits approach involves going trough a case/switch type of loop at run time which will slow things down enormously

No. The naive way to implement it is a zero-cost abstraction (in C++ parlance), meaning all the computation happens at compilation time.

I will look into it, but If I can get Union running as I intend I don’t have to write not a single new line of code

Just to make clear the reason why this exception gets thrown:

  • This happens on line Elems2D{C,M} = Union{CElems{C,M}, quad{C,M}} where {C,M}, specifically in the type application CElems{C,M}, where you try to apply C and M as type parameters to CElems.

  • As shown in my first message, above, CElems is just Union{}.

  • Thus CElems{C,M} is just (Union{}){C,M}, which obviously has to throw because Union{} doesn’t take any type parameters.

1 Like