Thinking about it some more, the big problem with covariance is that it
forces the confusion of two distinct meanings of P{B} for abstract B:

   1. The concrete instance of type P with actual parameter type B.
   2. The abstract type including all instances P{A} where A <: B.

If P{B} meant the latter, then we would either need a new way to express
the former or we would be forced to identify the two. Unlike static
languages, the concrete interpretation is not just a hallucination of the
compiler – it exists at run-time – so we can't just have no name for it –
there needs to be some concrete answer when you write typeof(x).
Identifying the two meanings is very un-Julian as it would allow a concrete
type to have subtypes, which would be a massive and to my mind distasteful
change.


On Tue, Apr 29, 2014 at 11:51 AM, Stefan Karpinski <ste...@karpinski.org>wrote:

> The real root issue here is parametric 
> variance<https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)>.
> Julia has opted for parametric invariance  – i.e. that P{A} <: P{B} only if
> A = B – which is what James mentions in his StackOverflow answer. The
> wikipedia page does a decent job of summarizing the issue:
>
> A programming language designer will consider variance when devising
>> typing rules for e.g. arrays, inheritance, and generic datatypes. By making
>> type constructors covariant or contravariant instead of invariant, more
>> programs will be accepted as well-typed. On the other hand, programmers
>> often find contravariance unintuitive, and accurately tracking variance to
>> avoid runtime type errors can lead to complex typing rules. In order to
>> keep the type system simple and allow useful programs, a language may treat
>> a type constructor as invariant even if it would be safe to consider it
>> variant, or treat it as covariant even when that can violate type safety.
>
>
> Julia has chosen the simple approach of invariance everywhere. This
> simplifies the subtyping rules, but there is no free lunch – variance
> issues still do crop up. But at least Julia's rules are simple if
> occasionally a bit inconvenient. Basically, this is the situation:
>
>    - *covariance:* *P{A} <: P{B} ⟺ A <: B* – correct for read, incorrect
>    for write.
>    - *contravariance:* *P{A} <: P{B} ⟺ B <: A* – correct for write,
>    incorrect for read.
>    - *invariance:* *P{A} <: P{B} ⟺ A = B* – correct for both read and
>    write.
>
> The main purpose of variance in a programming language is to allow
> definitions to apply to more things. In particular, since the number of
> types that a parametric type may encompass is unbounded, you don't want to
> have to write an infinite number of method definitions to define some
> functionality for P{A} for all A <: B where B is some unbounded abstract
> type. Instead of accomplishing this with covariance or contravariance,
> Julia accomplishes it with more powerful dispatch – specifically, method
> signatures with quantified type parameters, such as:
>
> frob{A<:B}(x::P{A})
>
>
> This is effectively a covariant signature, applying to P{A} for any A <:
> B. It seems like you would be happier if parametric types were covariant
> and you were allowed to write this as:
>
> frob(x::P{B})
>
>
> where the method implicitly also applies to x::P{A} for every A <: B. This
> would certainly be less of an eyesore. There are a couple of important
> points to be made, however:
>
>    1. In the longer, more explicit form, it is clear that B may not be
>    the actual parameter type of x; the parameter type of x is some A which is
>    a subtype of B.
>    2. If the shorter form were shorthand for the longer form without
>    explicit A, there would be no way to express that x has an
>    exact parameter type of B.
>
> The explicit distinction between A – the actual parameter type of x – and
> B, which is simply an upper bound on A, becomes clearer in situations like
> this:
>
> frob{A<:B}(x::P{A}, y::A)
> frob{A<:B}(x::P{A}, y::B)
>
>
> The first definition only applies to x and y where y is of the actual
> parameter type of x and thus could be used as is by x. The second
> definition applies when y is of type B, regardless of x's actual parameter
> type, which means that it might not be usable as is by x.
>
> We could actually make parametric types covariant without losing the
> ability to express this distinction; it would just mean that the second
> definition could be written as:
>
> frob(x::P{B}, y::B)
>
>
> Currently, this means that x must have exact parameter type B; if types
> were covariant then it would mean that x's parameter type could be any A <:
> B. Although it may seem questionable that y not be of the true parameter
> type of x, allowing this wiggle room and then doing conversion at run-time
> as necessary is actually the standard way to do things in Julia. Conversion
> is often done implicitly by assignment to a type location such as a field
> of x or an array slot. The fact that this is quite common is why covariance
> might actually be ok. The main thing we would lose with parametric
> covariance is the ability to talk about arrays that are of *exactly*
> element type B where B is some abstract type. But I'm just not sure how
> useful that actually is. Those types are generally discouraged in any case
> and it's rare to want to give them special behaviors that wouldn't also
> apply to parametric types with concrete parameters. So perhaps we ought to
> consider making parametric types in Julia covariant.
>
> On Tue, Apr 29, 2014 at 5:38 AM, Oliver Woodford <
> oliver.woodf...@gmail.com> wrote:
>
>> A habitual MATLAB user, I've been trying out Julia over the last two
>> weeks to see if it might be a suitable replacement. I want something that
>> is as fast to develop using, but has much faster runtime. Perhaps I'll
>> write about my general thoughts in another post. However, in this thread I
>> want to address one linguistic thing I found confusing.
>>
>> Ignoring subarrays, dense/sparse arrays, there are two main types of
>> array in Julia. I will call them homogenous and heterogenous. Homogenous
>> arrays are declared as having all elements be the same type: e.g.
>> array{Float64}. They are efficient to store in memory, as the elements are
>> simply laid out consecutively in memory. Heterogenous arrays have an
>> abstract element type, e.g. array{Real}. The way Julia interprets this is
>> that every element must be a concrete subtype of Real, but that they don't
>> have to be the same type. Each element can therefore be a different type,
>> with different storage requirements, so these arrays contain a pointer to
>> each element, which is then stored somewhere else - this carries a massive
>> overhead. In MATLAB these arrays would be termed an array and a cell array
>> respectively, so there is a clear distinction. What I found confusing with
>> Julia is that the distinction is less clear.
>>
>> This confusion was highlighted in a stackoverflow 
>> question<http://stackoverflow.com/questions/23326848/julia-arrays-with-abstract-parameters-cause-errors-but-variables-with-abstract>,
>> which I'll outline it again, now:
>>
>> f(x::Real) = x is equivalent to f{T<:Real}(x::T) = x, but f(x::Array{Real})
>> = x is different from f{T<:Real}(x::Array{T}) = x.
>>
>> The second form for input arrays, requiring static parameters, is needed
>> to declare that the array is homogenous, not heterogenous. This seems a
>> funny way of doing things to me because:
>> 1. The homogeneity/heterogeneity of the array is a characteristic of the
>> array only, and not of the function
>> 2. The static parameter T is not required anywhere else, and the Julia
>> style 
>> guide<http://julia.readthedocs.org/en/latest/manual/style-guide/#don-t-use-unnecessary-static-parameters>
>>  explicitly
>> counsels against the use of such parameters, where they are unnecessary.
>> 3. To declare a function which can take homogenous or heterogenous
>> arrays, I believe you'd have to do something like  
>> f{T<:Real}(x::Union(Array{T},
>> Array{Real})) = x, which seems totally bizarre (due to point 1).
>>
>> What I would advocate instead is two types of array, one homogenous, one
>> heterogenous. Array for homogenous and Cell for heterogenous would work.
>> It would do away with the need for static parameters in this case, and
>> also, in my view, make people far more aware of when they are using the
>> different types of array. I suspect many beginners are oblivious to the
>> distinction, currently.
>>
>> In the stackoverflow question, someone suggested two points against this:
>> 1. Having an array whose elements are all guaranteed to be some subtype
>> of Real is not particularly useful without specifying which subtype since
>> without that information almost no structural information is being provided
>> to the compiler (e.g. about memory layout, etc.)
>> Well, firstly I disagree; there is a lot of structural information being
>> supplied - to read each element, the compiler knows that it just needs to
>> compute an offset, rather than compute an offset, read a pointer and then
>> read another memory location. However, I don't think this is exploited
>> (though it could be) because the function will be recompiled from scratch
>> for each element type. Secondly, this isn't about helping the compiler,
>> it's about making the language more consistent and sensible - helping the
>> *user*.
>> 2. You almost always pass homogenous arrays of a concrete type as
>> arguments anyway and the compiler is able to specialize on that.
>> Firstly, homogenous arrays that you pass in *always *have a concrete
>> type. Secondly, you don't always know what that type will be. It might be
>> Float64 or Unit8, etc.
>>
>> I haven't yet heard a convincing counterargument to it making more sense
>> to distinguish homogenous and heterogenous arrays by the array type rather
>> than by static function parameter.
>>
>> Let the discussion begin...
>>
>
>

Reply via email to