Subtyping made friendly: Question about UnionAll

In Francesco Zappa Nardelli’s awesome talk, Subtyping made friendly, he showed this example to motivate UnionAll types, here:

However, those type signatures are not exactly equivalent: the second version of foo also accepts Vector{Signed} as an argument. This could be problematic if the implementation of foo depends on all the elements of the Vector having the same type.

Is there any way to achieve the same behavior as the original declaration without having to enumerate all the possibilities?

The behavior arises from <: being sort of like “less than or equal to”. Is there a way to specify a “strictly less than” version of the subtype relation for dispatch, like <<:?

Or perhaps some way to form the Union programmatically? Something like this pseudocode:

signed_types = Union{collect(Vector{T} where T <: Signed && T != Signed)...}
function foo(x::signed_types) ... end
1 Like
@assert isconcretetype(T)

in the function body.

That works okay, but not if I want the logic to happen during dispatch… Like, maybe I have some other generic version of foo I want to be called for all other types.

You can do that with traits (check the type for concreteness, compute a trait, dispatch on that).

That said, it would be interesting to see a use case, perhaps there is an easier way of doing what you want.

You could do the dispatch manually with an @generated function:

julia> @generated function f(x::Vector{T}) where {T <: Signed}
         if isconcretetype(T)
           :(f_concrete(x))
         else
           :(f_not_concrete(x))
         end
       end
f (generic function with 1 method)

julia> f_concrete(x) = "concrete"
f_concrete (generic function with 1 method)

julia> f_not_concrete(x) = "not concrete"
f_not_concrete (generic function with 1 method)

julia> f([1,2,3])
"concrete"

julia> f(Signed[1,2,3])
"not concrete"

Although the compiler is actually pretty smart, so the @generated isn’t even necessary in this case. The following works just as well:

julia> function f(x::Vector{T}) where {T <: Signed}
         if isconcretetype(T)
           f_concrete(x)
         else
           f_not_concrete(x)
         end
       end
f (generic function with 1 method)

Alternatively, you could use a trait:

julia> f(x::Vector{T}) where {T <: Signed} = f(x, mytrait(T))
f (generic function with 3 methods)

julia> struct ConcreteSigned; end

julia> struct NonConcreteSigned; end

julia> function mytrait(::Type{T}) where {T <: Signed}
         if isconcretetype(T)
           ConcreteSigned()
         else
           NonConcreteSigned()
         end
       end
mytrait (generic function with 2 methods)

julia> f(x::Vector{<:Signed}, ::ConcreteSigned) = "concrete"
f (generic function with 3 methods)

julia> f(x::Vector{<:Signed}, ::NonConcreteSigned) = "not concrete"
f (generic function with 3 methods)

julia> f([1,2,3])
"concrete"

julia> f(Signed[1,2,3])
"not concrete"
7 Likes

Thanks both for the great explanations!!

Ah, sorry, I don’t have one, I was simply curious about this while watching the above talk. It makes sense though I think that in most cases isconcretetype isn’t a necessary constraint.

Thanks!

Hi all.

The example in my talk was meant to give nothing more an than intuition about the semantics of UnionAll types. The two types are not equivalent (you cannot prove the latter to be subtype of the former) and I apologize if this was source of confusion.

Btw, the solution with traits is a nice use case for the Type type construct, which turned out to be tricky to formalize correctly. If you are curious, our paper contains all the details: Julia Subtyping: a Rational Reconstruction (we did our best to make it accessible to non programming-language-semantics specialists).

-francesco

9 Likes