Reason behind designing parametric types as invariant

In a sense we do:

If we look at our table of variances, we see that &mut T is invariant over T . As it turns out, this completely fixes the issue!

Since all types in Julia behave like &mut T in Rust (or at least there’s no way of expressing anything else in the type system—there are immutable types and reference types but they are all treated uniformly), that fact translated to Julia would imply that all parametric types are invariant, which is precisely what we do. In order to do something as fine-grained as what Rust does, you need as fine-grained and complex a type system, which we don’t have and have deemed not to be a good trade off for the kind of applications we want Julia to excel at.

In another sense Julia already has covariant and contravariant parametric types: P{<:T} is covariant in T and P{>:T} is contravariant in T. In other words if you want to write co/contravariant parametric types you can, it’s just that the safe invariant form is the default. We could have changed things (I’ve considered it) and made P{T} mean what P{<:T} means now. But then we’d need a way to write what P{T} means now. Maybe P{=:T}? (Not great since =:T is already syntax but it works in unary position.)

So covariant by default is a possibility, but what should be the default? Another question that comes up now and then is if we couldn’t allow subtyping concrete types. And we potentially could, but if we did what most languages do and allowed both subtyping of concrete types and made covariant the default for parametric types then we’d really be hoist since Vector{Float64} would be forced to be a pointer array and we’d lose all hope of being a good language for numerical computing. This is precisely the reason why primitives (double etc.) need to be special in Java and user-defined types are not as efficient as primitives. (See “value types” for a proposed way to try to remedy the situation in Java.)

But still, we could do one or the other and of the two I think that making parametric types covariant by default is the more reasonable but I’m still not sure that it’s a good idea. The main argument for it is that when someone writes

f(strs::Vector{AbstractString})

you often should have written

f(strs::Vector{<:AbstractString})

But then again, what if it was

f!(strs::Vector{AbstractString})

and f! tries to insert a string of a particular type into strs? Maybe that’s fine because Julia generally does automatic conversion for you in such situations which should actually work as desired. On the other hand, how hard is it to write

f!(strs::Vector{<:AbstractString})

if that’s what you want? Invariance is the simplest, least dangerous variance, so shouldn’t it be the default and easiest to express?

28 Likes