Parametric abstract types

I have an abstract type MyAbstractType and there’re several parametric concrete types “under” it. e.g.,

MyConcreteType1 <: MyAbstractType
MyConcreteType2 <: MyAbstractType
MyConcreteType3 <: MyAbstractType
...

Now, wanna define a function that accepts x and y: they’re of the same concrete type, but with (possibly) different type parameters. e.g. fun(x, y) where x is a MyConcreteType2{Float16} and y is a MyConcreteType2{Float64}.

any idea how to solve the following error? thanks.

function fun(x::S{U}, y::S{V}) where {S <: MyAbstractType,
                                        U <: AbstractFloat,
                                        V <: AbstractFloat}
    println(S)
    println(U)
    println(V)
end

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

You should make the type parameter part of the abstract type. e.g.

abstract type MyAbstractType{T} end
struct MyConcreteType1{T} <: MyAbstractType{T}
    ...
end

then you can do

function fun(x::MyAbstractType{U}, y::MyAbstractType{V}) where {U<:AbstractFloat, V<:AbstractFloat}
end

and it will match instances of your concrete type.

1 Like

unfortunately it does not work:

abstract type MyAbstractType{T <: AbstractFloat} end
struct MyConcreteType1{T} <: MyAbstractType{T} end
struct MyConcreteType2{T} <: MyAbstractType{T} end

function fun(x::MyAbstractType{U}, y::MyAbstractType{V}) where {U<:AbstractFloat, V<:AbstractFloat}
    println(typeof(x) )
    println(typeof(y) )
end

julia> x = MyConcreteType1{Float16}()
MyConcreteType1{Float16}()
julia> y = MyConcreteType2{Float32}()
MyConcreteType2{Float32}()
julia> z = MyConcreteType2{Float64}()
MyConcreteType2{Float64}()

julia> fun(x, y)
MyConcreteType1{Float16}
MyConcreteType2{Float32}

julia> fun(y, z)
MyConcreteType2{Float32}
MyConcreteType2{Float64}

julia> fun(x, z)
MyConcreteType1{Float16}
MyConcreteType2{Float64}

what I want is fun(x, y) and fun(x, z) being invalid (different concrete types) , while fun(y, z) is valid (same concrete type).

I need to do so because there’re a lot of concrete types under the abstract type, and I don’t want to define the function for each concrete types …

how to do?

I don’t know whether this is possible?

If you can write generic code (i.e. a single fun definition) for x::T{S} and x::T{U} for any T<:MyAbstractType, but not for two different subtypes, then maybe you need to re-think why they have a common abstract type in the first place? What is the point of the abstract type if subtypes can’t share code? Can you give a more specific example?

See also the Conversion and Promotion section of the manual for one approach to handling large number of combinations of types.

1 Like

a bit complicated… I’m defining something like a “tensor” (the abstract type), which could take different “shapes”. Each shape is then represented by a parametric (floating point representation) concrete type.

Now, for operations like “addition”, we need same shapes. What’s why I want the function described above (actually that function would do the promotion inside).

However, for operations like “multiplication”, some shape could do with another shape (that I’ve defined explicitly).

After all, there’re a lot of operations that are common to all shapes, e.g. function to get and set the dimensions. Maintaining an abstract type Tensor would be necessary to avoid defining very-similar functions across different shapes.

sigh… if it’s the case, I would just go to define the functions for each concrete type then.

Yes, what you initially wanted to do isn’t possible directly. To make this concrete, let’s consider AbstractArray{T}, with the concrete types Array{T,1} and UnitRange{T}. The syntax you wanted is this:

function fun(x::S{U}, y::S{V}) where {S <: AbstractArray, U, V}

But that doesn’t work because Julia needs to know exactly what the S is before it knows what S{U} or S{V} means. Now, yes, the T in Array{T} and UnitRange{T} happen to mean the same thing as the first parameter in their supertype, but that needn’t be the case. What would this function do when presented with BitArrays, for example? The first parameter of a BitArray is not its element type but rather its dimensionality! Would that then mean that it’d only accept BitArrays of the same dimensionality?

In other words, what you want to do doesn’t have a well-defined meaning. Instead of relying upon method errors, just check the cases in code and throw ArgumentErrors instead.

4 Likes

You could have the shape as part of the type parameter, instead of as distinct subtypes. e.g. you could parameterize by a tuple, much like how the SArray type in StaticArrays works.

Do you really need the shape check to be done at compile time? When you add two matrices, the shape check is done at runtime. Compile-time checks of sizes are mainly important for tiny arrays, i.e. something like the StaticArrays package.

2 Likes

Also note that encoding something into a single method signature is not the only way to do computation at compile time. If the sizes are encoded in the type domain, then the checks you write against them — be they in the top-level method signature or not — can be computed at compile time.

2 Likes

I mean, it may be difficult to implement, but I think it would be nice to be included in future versions of Julia. After all, a “human” is able to “parse” the following without ambiguity:

also, the following, in my understanding, represents that we need to re-think the syntax and meaning of parametric type. I think a good language should be as intuitive as possible.

how? e.g.

function fun(x::MyAbstractType{S}, y::MyAbstractType{T} ) where {S <: AbstractFloat, T <: AbstractFloat}
    # ??? how to check if x and y are of the same concrete type (but parameters S and T can be different) ???
end

in other words, how to get the “concrete type without parameter”?
using typeof(), e.g. typeof(randn(3) ) gives Array{Float64, 1}. But is there any function that would return Array simply???

Guess I’m not “human”. Bummer.

The point is simply that the concrete types are the ones that decide what its type parameters mean and how to report to the supertype what the supertype’s parameters are. Thus no-one is able to say what S{V} means unless they know what S is. That’s just the way things work. Reconsidering this design would introduce quite the limitation.

When I was talking about doing the check at compile time, I was thinking more in terms of the computation on the “shape” parameters — it’s indeed much easier to yank out the parameters than it is to ask for the type. One simple way, though, is to simply ask if the names are the same: nameof(typeof(x)) === nameof(typeof(y))) does its work at compile time but of course will fail if you have two subtypes with the same name but in different modules.

4 Likes