This question already has answers here:
Question about Julia type syntax: Why is Array{Int32, 1} <: Array{Integer, 1} false?
(3 answers)
Closed 1 year ago.
Context
In Julia: what does the "<:" symbol mean?, the 2nd answer directly quoted the three Julia's documentation for the <: operator. The 3rd one puzzles me.
Problem
To make this question independent from the quoted question, I'll make a particular example.
julia> 1.3 isa AbstractFloat
true
julia> [1.3 1.3; 1.2 1.2] isa Matrix{Float64}
true
julia> [1.3 1.3; 1.2 1.2] isa Matrix{AbstractFloat}
false
From Julia's documentation, AbstractFloat is an abstract type for all floating values, e.g. Float32, Float64, etc. That's explains the first example. However, I can't understand why a matrix of Float64 isn't a matrix of AbstractFloat.
You have to do:
julia> [1.3 1.3; 1.2 1.2] isa Matrix{<:AbstractFloat}
true
as type parameters in Julia are invariant.
This is explained in detail here.
If the linked explanation is not clear please comment where you need additional info and I can expand on the problematic areas.
EDIT
An explanation from a different angle (maybe it will be also helpful).
As is explained here in Julia:
One particularly distinctive feature of Julia's type system is that concrete types may not subtype each other: all concrete types are final and may only have abstract types as their supertypes. While this might at first seem unduly restrictive, it has many beneficial consequences with surprisingly few drawbacks.
The reason for this is that concrete types have a concrete memory layout, and in particular Matrix{Float64} and Matrix{AbstactFloat} have a different memory layout. Knowing memory layout of an object is crucial if we want the compiler to emit efficient machine code.
Now both Matrix{AbstractFloat} and Matrix{Float64} are concrete types (they can have instances):
julia> Matrix{AbstractFloat}(undef,0,0)
0×0 Matrix{AbstractFloat}
julia> Matrix{Float64}(undef,0,0)
0×0 Matrix{Float64}
julia> isconcretetype(Matrix{AbstractFloat})
true
julia> isconcretetype(Matrix{Float64})
true
In consequence Matrix{Float64} cannot be a subtype of Matrix{AbstractFloat} (nor vice versa)
Related
I try to understand typing in Julia and encounter the following problem with Array. I wrote a function bloch_vector_2d(Array{Complex,2}); the detailed implementation is irrelevant. When calling, here is the complaint:
julia> bloch_vector_2d(rhoA)
ERROR: MethodError: no method matching bloch_vector_2d(::Array{Complex{Float64},2})
Closest candidates are:
bloch_vector_2d(::Array{Complex,2}) at REPL[56]:2
bloch_vector_2d(::StateAB) at REPL[54]:1
Stacktrace:
[1] top-level scope at REPL[64]:1
The problem is that an array of parent type is not automatically a parent of an array of child type.
julia> Complex{Float64} <: Complex
true
julia> Array{Complex{Float64},2} <: Array{Complex,2}
false
I think it would make sense to impose in julia that Array{Complex{Float64},2} <: Array{Complex,2}. Or what is the right way to implement this in Julia? Any helps or comments are appreciated!
This issue is discussed in detail in the Julia Manual here.
Quoting the relevant part of it:
In other words, in the parlance of type theory, Julia's type parameters are invariant, rather than being covariant (or even contravariant). This is for practical reasons: while any instance of Point{Float64} may conceptually be like an instance of Point{Real} as well, the two types have different representations in memory:
An instance of Point{Float64} can be represented compactly and efficiently as an immediate pair of 64-bit values;
An instance of Point{Real} must be able to hold any pair of instances of Real. Since objects that are instances of Real can be of arbitrary size and structure, in practice an instance of Point{Real} must be represented as a pair of pointers to individually allocated Real objects.
Now going back to your question how to write a method signature then you have:
julia> Array{Complex{Float64},2} <: Array{<:Complex,2}
true
Note the difference:
Array{<:Complex,2} represents a union of all types that are 2D arrays whose eltype is a subtype of Complex (i.e. no array will have this exact type).
Array{Complex,2} is a type that an array can have and this type means that you can store Complex values in it that can have mixed parameter.
Here is an example:
julia> x = Complex[im 1im;
1.0im Float16(1)im]
2×2 Array{Complex,2}:
im 0+1im
0.0+1.0im 0.0+1.0im
julia> typeof.(x)
2×2 Array{DataType,2}:
Complex{Bool} Complex{Int64}
Complex{Float64} Complex{Float16}
Also note that the notation Array{<:Complex,2} is the same as writing Array{T,2} where T<:Complex (or more compactly Matrix{T} where T<:Complex).
This is more of a comment, but I can't hesitate posting it. This question apprars so often. I'll tell you why that phenomenon must arise.
A Bag{Apple} is a Bag{Fruit}, right? Because, when I have a JuicePress{Fruit}, I can give it a Bag{Apple} to make some juice, because Apples are Fruits.
But now we run into a problem: my fruit juice factory, in which I process different fruits, has a failure. I order a new JuicePress{Fruit}. Now, I unfortunately get delivered a replacement JuicePress{Lemon} -- but Lemons are Fruits, so surely a JuicePress{Lemon} is a JuicePress{Fruit}, right?
However, the next day, I feed apples to the new press, and the machine explodes. I hope you see why: JuicePress{Lemon} is not a JuicePress{Fruit}. On the contrary: a JuicePress{Fruit} is a JuicePress{Lemon} -- I can press lemons with a fruit-agnostic press! They could have sent me a JuicePress{Plant}, though, since Fruits are Plants.
Now we can get more abstract. The real reason is: function input arguments are contravariant, while function output arguments are covariant (in an idealized setting)2. That is, when we have
f : A -> B
then I can pass in supertypes of A, and end up with subtypes of B. Hence, when we fix the first argument, the induced function
(Tree -> Apple) <: (Tree -> Fruit)
whenever Apple <: Fruit -- this is the covariant case, it preserves the direction of <:. But when we fix the second one,
(Fruit -> Juice) <: (Apple -> Juice)
whenever Fruit >: Apple -- this inverts the diretion of <:, and therefore is called contravariant.
This carries over to other parametric data types, since there, too, you usually have "output-like" parameters (as in the Bag), and "input-like" parameters (as with the JuicePress). There can also be parameters that behave like neither (e.g., when they occur in both fashions) -- these are then called invariant.
There are now two ways in which languages with parametric types solve this problem. The, in my opinion, more elegant one is to mark every parameter: no annotation means invariant, + means covariant, - means contravariant (this has technical reasons -- those parameters are said to occur in "positive" and "negative position"). So we had the Bag[+T <: Fruit], or the JuicePress[-T <: Fruit] (should be Scala syntax, but I haven't tried it). This makes subtyping more complicated, though.
The other route to go is what Julia does (and, BTW, Java): all types are invariant1, but you can specify upper and lower unions at the call site. So you have to say
makejuice(::JoicePress{>:T}, ::Bag{<:T}) where {T}
And that's how we arrive at the other answers.
1Except for tuples, but that's weird.
2This terminology comes from category theory. The Hom-functor is contravariant in the first, and covariant in the second argument. There's an intuitive realization of subtyping through the "forgetful" functor from the category Typ to the poset of Types under the <: relation. And the CT terminology in turn comes from tensors.
While the "how it works" discussion has been done in the another answer, the best way to implement your method is the following:
function bloch_vector_2d(a::AbstractArray{Complex{T}}) where T<:Real
sum(a) + 5*one(T) # returning something to see how this is working
end
Now this will work like this:
julia> bloch_vector_2d(ones(Complex{Float64},4,3))
17.0 + 0.0im
I try to understand typing in Julia and encounter the following problem with Array. I wrote a function bloch_vector_2d(Array{Complex,2}); the detailed implementation is irrelevant. When calling, here is the complaint:
julia> bloch_vector_2d(rhoA)
ERROR: MethodError: no method matching bloch_vector_2d(::Array{Complex{Float64},2})
Closest candidates are:
bloch_vector_2d(::Array{Complex,2}) at REPL[56]:2
bloch_vector_2d(::StateAB) at REPL[54]:1
Stacktrace:
[1] top-level scope at REPL[64]:1
The problem is that an array of parent type is not automatically a parent of an array of child type.
julia> Complex{Float64} <: Complex
true
julia> Array{Complex{Float64},2} <: Array{Complex,2}
false
I think it would make sense to impose in julia that Array{Complex{Float64},2} <: Array{Complex,2}. Or what is the right way to implement this in Julia? Any helps or comments are appreciated!
This issue is discussed in detail in the Julia Manual here.
Quoting the relevant part of it:
In other words, in the parlance of type theory, Julia's type parameters are invariant, rather than being covariant (or even contravariant). This is for practical reasons: while any instance of Point{Float64} may conceptually be like an instance of Point{Real} as well, the two types have different representations in memory:
An instance of Point{Float64} can be represented compactly and efficiently as an immediate pair of 64-bit values;
An instance of Point{Real} must be able to hold any pair of instances of Real. Since objects that are instances of Real can be of arbitrary size and structure, in practice an instance of Point{Real} must be represented as a pair of pointers to individually allocated Real objects.
Now going back to your question how to write a method signature then you have:
julia> Array{Complex{Float64},2} <: Array{<:Complex,2}
true
Note the difference:
Array{<:Complex,2} represents a union of all types that are 2D arrays whose eltype is a subtype of Complex (i.e. no array will have this exact type).
Array{Complex,2} is a type that an array can have and this type means that you can store Complex values in it that can have mixed parameter.
Here is an example:
julia> x = Complex[im 1im;
1.0im Float16(1)im]
2×2 Array{Complex,2}:
im 0+1im
0.0+1.0im 0.0+1.0im
julia> typeof.(x)
2×2 Array{DataType,2}:
Complex{Bool} Complex{Int64}
Complex{Float64} Complex{Float16}
Also note that the notation Array{<:Complex,2} is the same as writing Array{T,2} where T<:Complex (or more compactly Matrix{T} where T<:Complex).
This is more of a comment, but I can't hesitate posting it. This question apprars so often. I'll tell you why that phenomenon must arise.
A Bag{Apple} is a Bag{Fruit}, right? Because, when I have a JuicePress{Fruit}, I can give it a Bag{Apple} to make some juice, because Apples are Fruits.
But now we run into a problem: my fruit juice factory, in which I process different fruits, has a failure. I order a new JuicePress{Fruit}. Now, I unfortunately get delivered a replacement JuicePress{Lemon} -- but Lemons are Fruits, so surely a JuicePress{Lemon} is a JuicePress{Fruit}, right?
However, the next day, I feed apples to the new press, and the machine explodes. I hope you see why: JuicePress{Lemon} is not a JuicePress{Fruit}. On the contrary: a JuicePress{Fruit} is a JuicePress{Lemon} -- I can press lemons with a fruit-agnostic press! They could have sent me a JuicePress{Plant}, though, since Fruits are Plants.
Now we can get more abstract. The real reason is: function input arguments are contravariant, while function output arguments are covariant (in an idealized setting)2. That is, when we have
f : A -> B
then I can pass in supertypes of A, and end up with subtypes of B. Hence, when we fix the first argument, the induced function
(Tree -> Apple) <: (Tree -> Fruit)
whenever Apple <: Fruit -- this is the covariant case, it preserves the direction of <:. But when we fix the second one,
(Fruit -> Juice) <: (Apple -> Juice)
whenever Fruit >: Apple -- this inverts the diretion of <:, and therefore is called contravariant.
This carries over to other parametric data types, since there, too, you usually have "output-like" parameters (as in the Bag), and "input-like" parameters (as with the JuicePress). There can also be parameters that behave like neither (e.g., when they occur in both fashions) -- these are then called invariant.
There are now two ways in which languages with parametric types solve this problem. The, in my opinion, more elegant one is to mark every parameter: no annotation means invariant, + means covariant, - means contravariant (this has technical reasons -- those parameters are said to occur in "positive" and "negative position"). So we had the Bag[+T <: Fruit], or the JuicePress[-T <: Fruit] (should be Scala syntax, but I haven't tried it). This makes subtyping more complicated, though.
The other route to go is what Julia does (and, BTW, Java): all types are invariant1, but you can specify upper and lower unions at the call site. So you have to say
makejuice(::JoicePress{>:T}, ::Bag{<:T}) where {T}
And that's how we arrive at the other answers.
1Except for tuples, but that's weird.
2This terminology comes from category theory. The Hom-functor is contravariant in the first, and covariant in the second argument. There's an intuitive realization of subtyping through the "forgetful" functor from the category Typ to the poset of Types under the <: relation. And the CT terminology in turn comes from tensors.
While the "how it works" discussion has been done in the another answer, the best way to implement your method is the following:
function bloch_vector_2d(a::AbstractArray{Complex{T}}) where T<:Real
sum(a) + 5*one(T) # returning something to see how this is working
end
Now this will work like this:
julia> bloch_vector_2d(ones(Complex{Float64},4,3))
17.0 + 0.0im
I'm trying to understand the structure of arrays in the Julia Type Graph. This seems very counter-intuitive to me:
julia> Int64 <: Number
true
julia> Array{Int64,1} <: Array{Number,1}
false
julia> Array{Int64,1} <: Array{Int,1}
true
It seems that a <: b is not sufficient for Array{a,1} <: Array{b,1}. When does Array{a,1} <: Array{b,1}?
A practical corollary: how can I type-declare an abstract array of numbers?
In the following page of the manual, it's described how julia's types are invariant as opposed to covariant.
https://docs.julialang.org/en/v1/manual/types/#Parametric-Composite-Types-1
See in particular the warning admonition stating
This last point is very important: even though Float64 <: Real we DO NOT have Point{Float64} <: Point{Real}.
And the following explanation given
In other words, in the parlance of type theory, Julia's type parameters are invariant, rather than being covariant (or even contravariant). This is for practical reasons: while any instance of Point{Float64} may conceptually be like an instance of Point{Real} as well, the two types have different representations in memory:
An instance of Point{Float64} can be represented compactly and efficiently as an immediate pair of 64-bit values;
An instance of Point{Real} must be able to hold any pair of instances of Real. Since objects that are instances of Real can be of arbitrary size and structure, in practice an instance of Point{Real} must be represented as a pair of pointers to individually allocated Real objects.
An abstract array with any kind of number is denoted like this
AbstractArray{<:Number} which is short for AbstractArray{T} where T <: Number
In Julia,
Array{Int32, 1} <: Array{Integer, 1}
evaluates to false, but
Array{Int32, 1} <: (Array{T, 1} where T <: Integer)
evaluates to true because Int32 <: Integer is true.
In my mind, the first and second expressions communicate the same idea and should evaluate equivalently. Furthermore, the first expression is less cluttered. Is there a reason why Julia's syntax evaluates the former as false but the latter as true? Is there something deep and good about this behavior or is this an oversight in how Arrays/the type system were developed?
See Parametric Composite Types from the manual.
Concrete Point types with different values of T are never subtypes of each other:
julia> Point{Float64} <: Point{Int64}
false
julia> Point{Float64} <: Point{Real}
false
Warning
This last point is very important: even though Float64 <: Real we DO NOT have Point{Float64} <: Point{Real}.
In other words, in the parlance of type theory, Julia's type parameters are invariant, rather than being covariant (or even contravariant).
This is for practical reasons: while any instance of Point{Float64} may conceptually be like an instance of Point{Real} as well, the two types have different representations in memory:
An instance of Point{Float64} can be represented compactly and efficiently as an immediate pair of 64-bit values;
An instance of Point{Real} must be able to hold any pair of instances of Real. Since objects that are instances of Real can be of arbitrary size and structure, in practice an instance of Point{Real} must be represented as a pair of pointers to individually allocated Real objects.
The key difference between Array{Integer, 1} and Array{T, 1} where T <: Integer is that the former is a concrete type, while the later is an abstract type. The reason this makes a difference is that you can make a variable with type Array{Integer, 1}. This is a potentially heterogeneous array, so it has to be implemented as an array of pointers (so slow and heap allocated). With this in mind, it is clear why Array{Int32, 1} <: Array{Integer, 1}=false. If we write a method for the specific type Array{Integer, 1}, that can't be specialized since it is already a concrete type, and we would segfault when we run it on an Array{Int32, 1} which has a completely different data format (inline elements).
There were discussions about making the behaviour you described as default, but they finally reached consensus that the current might be better.
Basically it is a syntax choice of what Vector{Integer} means. There are three types of vairance to choose from:
julia> Vector{Int32} <: (Vector{T} where T <: Integer)
true
julia> Vector{Any} <: (Vector{T} where T >: Integer)
true
julia> Vector{Integer} <: (Vector{T} where {T >: Integer, T <: Integer})
true
The three types of variance correspond to three different operations about the variable. Covariance for read, contravariance for write, and invariance for both. For example in Rust, &'a T is covariant about T, because you can only read from it, while &'a mut T is invariant since it can be both read and written. If ever there being a "write-only" type, contravarance would be a sane default. Given variables in Julia are both read and writable, defaulting to invariance looks a good choice.
In Julia, composite types with at least one field that have identical values hash to different values. This means composite types don't function correctly if you use them as dictionary keys or anything else that is dependent on the hashed value. This behavior is inconsistent with the behavior of other types, such as Vector{Int} as well.
More concretely,
vectors of non-composite types that are different objects but have identical values hash to the same value:
julia> hash([1,2,3])==hash([1,2,3])
true
composite types with no fields hash to the same value:
julia> type A end
julia> hash(A())==hash(A())
true
composite types with at least one field hash to different values if they're different objects that have the same value:
julia> type B
b::Int
end
julia> hash(B(1))==hash(B(1))
false
however, the same object maintains its hash even if the underlying values change:
julia> b=B(1)
julia> hash(b)
0x0b2c67452351ff52
julia> b.b=2;
julia> hash(b)
0x0b2c67452351ff52
this is inconsistent with the behavior of vectors (if you change an element, the hash changes):
julia> a = [1,2,3];
julia> hash(a)
0xd468fb40d24a17cf
julia> a[1]=2;
julia> hash(a)
0x777c61a790f5843f
this issue is not present for immutable types:
julia> immutable C
c::Int
end
julia> hash(C(1))==hash(C(1))
true
Is there something fundamental driving this behavior from a language design perspective? Are there plans to fix or correct this behavior?
I'm not a Julia language designer, but I'l say this sort of behavior is not surprising when comparing mutable and immutable values. Your type B is mutable: it's not entirely obvious that two instances of it, even if they have the same value for field b, should be considered equal. If you feel like this should be the case, you are free to implement a hash function for it. But in general, mutable objects have independent identities. Immutable entities, like C are impossible to tell apart from each other, therefore it makes sense for them to obey structural hashing.
Two bank accounts that happen to have $5 in them are not identical and probably shouldn't hash to the same number. But two instances of $5 are impossible to distinguish from each other.