# Parameterize by Value with Union

I’d like to use a struct to represent a spatial mesh with an arbitrary number of dimensions D, something like

``````struct Mesh{D}
...
end
``````

Of course, D can only be 1, 2, or 3. Is there a way to enforce this using a union, e.g.,

``````struct Mesh{D<:Union{1,2,3}}
...
end
``````

which would work if the Union involved types, but not values.

A simple way would be to define an inner constructor which checks the value of `D`, but I’m also curious if there is a better approach

3 Likes

You could wrap the values in `Val` to lift them to the type domain. Then you could do:

``````struct Mesh{D<:Union{Val{1},Val{2},Val{3}}}
...
end
``````

`Base` does something similar with `AbstractVecOrMat` which is just an alias for `Union{AbstractArray{T, 1}, AbstractArray{T, 2}}`.

So perhaps just define an alias like

``````struct ArbitraryMesh{D}
...
end

const Mesh = Union{ArbitraryMesh{1}, ArbitraryMesh{2}, ArbitraryMesh{3}}

dims(::ArbitraryMesh{D}) where D = D
``````

Then `dims(m::Mesh)` will give the number of dimensions statically.

1 Like

Splatting an iterable can reduce retyping when there are more cases or the name needs to change: `Union{(ArbitraryMesh{i} for i in 1:3)...}`. I would prefer something like `struct Mesh{D} end; const MeshTrio = Union{(Mesh{i} for i in 1:3)...}` because writing `Mesh{2}` is easier than `ArbitraryMesh{2}`, and you can’t tack a parameter onto a Union to retrieve one of its members.

AFAIK, what densmojd is thinking of seems impossible now. Inner constructors generally check constraints because redefinable methods and mutable instances can change what constraint checks do, yet type annotations and previous instances of the type can’t feasibly change with them and must use the “obsolete” types. Subtype checking is a built-in function `<:`, and types can’t be changed, so those are fine to incorporate into the type. `isa` is also a built-in function, so that seems feasible as a future parameter constraint. On the other hand, membership checking is a generic function `in`. `in(::Any, ::Tuple)` falls back to an iteration-based `in(x, itr)` method, so there’s no underlying built-in function to use instead. We’d also need something semantically different for parameter membership because `missing in (1, missing)` is `missing`, not `true`. If parameter constraints are ever expanded beyond subtyping checks, such a built-in function will probably show up.

2 Likes

A few alternatives:

### Parameterize a singleton type with an `Int` value

``````struct Mesh{D<:Union{Val{1},Val{2},Val{3}}} end
``````

If code duplication is an issue, you could do this:

``````struct Mesh{D<:Union{map(i -> Val{i}, 1:3)...,}} end
``````

### Hardcoded singleton types, each representing an integer

``````struct N1 end
struct N2 end
struct N3 end
struct Mesh{D<:Union{N1,N2,N3}} end
``````

### Set-theoretic construction of the natural numbers

This is like the above, but avoids hardcoding the values of the natural numbers. Inspired by a construction of the natural numbers, Zermelo ordinals.

``````module Naturals

# Arbitrarily choose to start counting from zero
struct Zero end

struct Natural{Previous} end

successor(::M) where {M<:Union{Zero,Natural}} = Natural{M}()

natural(::Val{0}) = Zero()

function natural(::Val{N}) where {N}
N::Int
signbit(N) && throw(ArgumentError("negative"))
successor(natural(Val(N - 1)))::Natural
end

natural(n::Int) = natural(Val(n))::Union{Zero,Natural}

natural_type(n::Union{Int,Val}) = typeof(natural(n))

end

struct Mesh{D<:Union{map(i -> Naturals.natural_type(i), 1:3)...,}} end

``````
2 Likes

Thanks for all the ideas! The suggestions of @abraemer and @nsajko are what I had in mind.

Of course, the follow up question is can I somehow extract the dimensions from the parameter, e.g.,

``````function numdim(mesh::Mesh{D}) where D
# return 1, 2, or 3 based on whether D is a Val{1}, Val{2}, or Val{3}
end
``````

I’m also beginning to think gdalle’s* suggestion to throw an assertion from an inner constructor is the better approach.

*Apparently new users can only mention 2 other users in a post.

``````julia> struct Mesh{D<:Union{Val{1},Val{2},Val{3}}} end

julia> numdim(mesh::Mesh{Val{D}}) where {D} = D
numdim (generic function with 1 method)

julia> numdim(Mesh{Val{2}}())
2
``````

Why?