How to subtype a type alias?

Hello everyone,

I have a struct:

struct MyType
    data :: Int
end

Now I want to create a vector of such structs:

const MyVector = Vector{MyType}

This works fine because I can make methods that dispatch on MyVector.

myfunction(::MyVector) = "some vector"

And it is very easy to create instances of that vector:

myvector = [MyType(1), MyType(2)]

My problem is that I would like to create an abstract type of which MyVector is subtype.
I don’t see how to do that.
I could, however make a wrapper type

abstract type AbstractStructure end
struct MyVector2 <: AbstractStructure
    data :: Vector{MyType}
end

But then to construct an instance I have to do

myvector2 = MyVector2([MyType(1), MyType(2)])

which I find a bit clumsy.
Is there a way to make

myvector2 =  [MyType(1), MyType(2)]

where

typeof(myvector2)<: AbstractStructure
true

?
Many thanks in advance,
Olivier

1 Like

What did you want this ability for? Is it for dispatch? If so, one avenue to get the behaviour you want (if not the syntax) is to use so-called trait based dispatch:

struct MyType
    data :: Int
end

const MyVector = Vector{MyType}
const MyMatrix = Matrix{MyType}

struct IsMyArray{T} end
struct Not{T} end 

ismyarray(::T) where {T} = Not{IsMyArray{T}}
ismyarray(::MyVector)    = IsMyArray{MyVector}
ismyarray(::MyMatrix)    = IsMyArray{MyMatrix}

f(x) = f(ismyarray(x), x)
f(::Type{IsMyArray{T}}, x) where {T} = "$T is one of my arrays"
f(::Type{Not{IsMyArray{T}}}, x) where {T} = "$T is not one of my arrays"
julia> f([MyType(1), MyType(2)])
 "Array{MyType,1} is one of my arrays"

julia> f([MyType(1), 2])
 "Array{Any,1} is not one of my arrays"

 julia> f([MyType(1); MyType(2)])
 "Array{MyType,1} is one of my arrays"

 julia> f([MyType(1)  MyType(2);
           MyType(3)  MyType(4)])
 "Array{MyType,2} is one of my arrays"

What is being done here is that we’ve invented a ‘trait’, IsMyArray{T} that is kinda like an abstract type but it doesn’t actually exist in the formal type hierarchy, but using the function ismyarray, we can still dispatch on the ‘members’ of this trait which in this case are MyVector and MyArray.

What I like about this approach is that it’s very flexible and can be extended by others downstream of you. You can even use it to unify objects from unrelated type heirarchies. Ie. you could make NTuple{N, MyType} where {N} a member of IsMyArray if you like, even though NTuple isn’t an AbstractArray and is not a type that you own. What I don’t like is that it’s a bit syntactically burdensome to set up.

There’s a package called SimpleTraits.jl which attempts to automate / smooth this process but I often find it a bit more confusing than just writing the trait by hand. Your milage may vary.

8 Likes

Hi,
Thanks a lot for this answer. I never thought about using traits in this case.
I’m very ignorant about traits though. Is it possible to create a supertype to the trait somehow?

With the solution I’m using now:

const MyVector = Vector{MyType}

I can already dispatch. So that is not really the issue.
What I need is to be able to define different structures which are subtypes of AbstractStructure in order to be able to write a generic function.

I have different structures:

abstract type AbstractStructure end

struct Bulk <: AbstractStructure end
struct MultiLayer <: Abstractstructure end

I just want to write

function absorption(structure :: AbstractStructure)
    r= reflection(structure)
    t= transmission(structure)
   return 1-r-t
end

where I have the methods

reflection(a :: Bulk) = 0.1
reflection(a :: MultiLayer) = 0.2
transmission(a :: Bulk) = 0.2
transmission(a :: MultiLayer) = 0.3

In my case MultiLayer is of the type Vector{MyType} which I don’t see how to make it subtype of AbstractStructure such as to keep absorption function generic.

To solve this I see 2 (3?) approaches

  • Using const MultiLayer = Vector{MyType} and create an union type Union{Bulk,MultiLayer}, but this is not easily extendable and I have to modify the union type.
  • Wrapping Vector{MyType} in the MultiLayer struct but then I don’t see how I can construct my vector like [MyType(1), MyType(2)]
  • Using traits, but then I have the same problem as in option 1, or maybe not?

So is it possible to create a vector const MultiLayer = Vector{MyType} for which?

myvector2 =  [MyType(1), MyType(2)]
typeof(myvector2)<: AbstractStructure
true

Thanks again,

Is it possible to create a supertype to the trait somehow?

A trait already is like a supertype. So in the example I showed above, we have the trait IsMyArray{T} and we can add members to it by just adding methods to the ismyarray function.

I still find your example quite unclear, and from what you say I suspect you can get away with something much simpler, but I’m going to assume the actual functionality you need is something like an abstract type. In that case, I’ll try again to give an example that might answer your question using traits.

First, lets define our types and aliases:

struct Bulk end

struct MyType
    data::Float64
end

const MultiLayer = Vector{MyType}

Now, lets define a trait called Structure, and make a function has_structure_trait that can decide if a given type is a member of our trait. For now, we’ll only register Bulk and MultiLayer as members, but we can add more whenever we like.

struct Structure{T} end
struct Not{T} end 

has_structure_trait(::T) where {T} = Not{Structure{T}}()
has_structure_trait(::MultiLayer)  = Structure{MultiLayer}()
has_structure_trait(::Bulk)        = Structure{Bulk}()

Now we can define our functions. The main difference from regular dispatch rules is that we’ll need functions that send their input to has_structure_trait to decide if they’re kosher to dispatch on:

absorption(structure) = absorption(has_structure_trait(structure), structure)
function absorption(::Structure{T}, structure) where {T}
    r= reflection(structure)
    t= transmission(structure)
   return 1-r-t
end

reflection(s)   = reflection(has_structure_trait(s), s)
reflection(::Structure{Bulk}, s::Bulk) = 0.1
reflection(::Structure{MultiLayer}, s::MultiLayer) = sum(x -> (x.data), s)

transmission(s) = transmission(has_structure_trait(s), s)
transmission(::Structure{Bulk}, s::Bulk) = 0.2
transmission(::Structure{MultiLayer}, s::MultiLayer) = sum(x -> 2*(x.data), s)

Now lets test it at the REPL.

julia> absorption(Bulk())
0.7

julia> absorption([MyType(0.1), MyType(0.2)])
0.09999999999999987

julia> absorption([1, 2])
ERROR: MethodError: no method matching absorption(::Not{HasStructureTrait{Array{Int64,1}}}, ::Array{Int64,1})
Closest candidates are:
  absorption(::HasStructureTrait{T}, ::T) where T at REPL[10]:2
  absorption(::Any) at REPL[9]:1
Stacktrace:
 [1] absorption(::Array{Int64,1}) at ./REPL[9]:1
 [2] top-level scope at none:0

If we want to improve that error message, we can do this:

julia> absorption(::Not{Structure{T}}, x::T) where {T} = throw("$T does not have the Structure Trait.")
absorption (generic function with 4 methods)

julia> absorption([1,2])
ERROR: "Array{Int64,1} does not have the Structure Trait."
Stacktrace:
 [1] absorption(::Not{HasStructureTrait{Array{Int64,1}}}, ::Array{Int64,1}) at ./REPL[19]:1
 [2] absorption(::Array{Int64,1}) at ./REPL[9]:1
 [3] top-level scope at none:0

So what we’ve done is made a trait called Structure{T} and we can add members to it using the has_structure_trait function. That can be done at any time and is very extensible. It is analogous to defining an abstract type, except it has fewer restrictions, and we can even do it to types we don’t own.

The downside is that we have to introduce methods like

absorption(structure) = absorption(has_structure_trait(structure), structure)

which decide if a certain structure gets passed on to

absorption(::Structure{T}, structure::T) where {T}

and this can add a fair amount of verbosity as you can see.

Basically the way to think about traits is that we are hacking the dispatch system so that we can just write a function has_structure_trait which decides what method gets used on a certain piece of data instead of the normal rules from the type system.


Edits:
I’ve included some clarity improvements and the suggestion from @Tamas_Papp to dispatch on values instead of types.

5 Likes

Thanks a lot for taking the time to explain this in so much detail.
But I think I need some time to digest it :wink:

I’m sorry if I was not totally clear.
I’m not sure if I can explain it much better though.

Would you mind to give a hint of what a simpler solution could be?

No worries, traits are something I’ve known vaguely about for a while but never really spent the time to use or implement them, so this was an instructive exercise for me as well.

I guess I’m just not sure if you really need the power of traits or even abstract types here, but its hard to know without seeing the larger context of what you’re actually tying to achieve. For instance, in the example you gave me, there’s no real reason why absorption needs some sort of restricted dispatch. Ie. your example would work fine if you just did

struct MyType
    data :: Int
end
const MultiLayer = Vector{MyType}

struct Bulk end

function absorption(structure) # Note that I've remove the type parameter here
    r = reflection(structure)
    t = transmission(structure)
   return 1-r-t
end

reflection(a::Bulk) = 0.1
reflection(a::MultiLayer) = 0.2
transmission(a::Bulk) = 0.2
transmission(a::MultiLayer) = 0.3

Now everything works and there’s no need for abstract types or traits. The function absorption will fail on any type other than Bulk or MultiLayer because reflection and transmission don’t have methods for any other types.

Of course, such an approach could fall apart depending on what you’re actually trying to do in practice.

Also, let me know if there’s something in the second example that’s still unclear to you. I can try and expand on that.

Nice example, but not that another style (possibly more commonly used) is returning values, not types, and dispatch accordingly, eg

has_structure_trait(::T) where {T} = Not{HasStructureTrait{T}}()

function absorption(::HasStructureTrait{T}, structure) where {T}
    ...
2 Likes

Good point. I’ll update my code above with that and a couple other legibility improvements.

Thanks again for your detailed explanation.
And indeed, I didn’t realize I don’t really need an abstract type!
That is quite an eye opener.
I have to check my code to see if I can totally get rid of it.

Just a small question related to

has_structure_trait(::T) where {T} = Not{HasStructureTrait{T}}

Why couldn’t you just do?

has_structure_trait(::T) where {T} = Not{T}

This seems to work also.

Many thanks again!

The Not{HasStructureTrait{T}} is important for if you have multiple traits laying around and you want to distinguish between them. Like for instance, imagine using fallback methods

f(::Not{HasStructureTrait{T}}, x::T) where {T} = g(x)
f(::Not{HasSomeOtherTrait{T}}, x::T) where {T} = h(x) 

Ok, I see.
Thanks!

I assume what @Olivier_Merchiers meant was something similar to the effect that in Base (array.jl) Matrix is defined on line 66 as

const Matrix{T} = Array{T,2}

where following type dependency is true

Matrix{T} <: AbstractMatrix{T}

without it anywhere being explicitly defined at that particular point, but indirectly implying a hidden hierarchy. It seems to pop up out of nowhere, which greatly confused me at first.
However, just a bit above on line 31 we have

const AbstractMatrix{T} = AbstractArray{T,2}

which then results in the type dependancy! This comes down to the very simple fact that Array{T,2} <: AbstractArray{T,2}, which Matrix{T} <: AbstractMatrix{T} would evaluate to.

Thus, if you have

struct MyType
    data :: Int
end

and

const MyVector = Vector{MyType}

you can simply define

const AbstractMyVector{T<:MyType} = AbstractVector{T}

and now MyVector <: AbstractMyVector.

BUT I don’t see the point in doing that to solve your problem (which you already solved anyway). You would then be dispatching on AbstractVector where you restricted T<:MyType. You can do that without the alias and you couldn’t extend this hierarchy in any meaningful way anyway. (I assume you wanted a supertype, which is not necessarily an AbstractVector.)

I even find these “parallel” or “shadow” hierarchies very confusing, because without looking into Base or maybe help or quickly printing the “type” in REPL you never know which types are actual structs/types and which are simply aliases.

1 Like

Hi,

Sorry I just realized I never replied!
Thanks a lot for this great explanation!

1 Like