-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
type system design iteration #8974
Comments
+100 |
This will be one heck of an improvement. I'm not sure I fully appreciate your intentions with |
Currently the scoping of the variable in typevars is both vague and limited. By explicitly wrapping them in a universal quantifier – i.e. |
This sounds great, but can you elaborate on the part
When does it apply? Which positions are considered covariant and which invariant? (Are there any contravariant positions?) Is that different in any way from making type parameters always invariant? |
@toivoh the idea was to describe how diagonal dispatch currently works. Consider: f{T}(x::T, y::T) = x + y#matches a concrete type like T = Float64 but not an abstract type like T = Real
f{T}(A::Matrix{T}, b::T) = A .+ b #matches both T = Float64 and T = Real in 0.4-dev but only T = Float64 in 0.3 The first method has input signature f(3.0, 4.0) #arguments of type (Float64, Float64)
f(3, 4.0) #arguments of type (Int, Float64) <: (Real, Real) but does not match The second has signature f(zeros(Real, 3, 3), 4.0) #arguments of type (Matrix{Real}, Float64) <: (Matrix{Real}, Real) The general rules appear to be:
The v0.4 rule is admittedly strange when written out like that. |
I don't think it's the whole tuple that's matched invariantly, just the typevar. For example |
The new covariant behavior of I agree that it's useful to be able to match e.g. I understand the rule from the point of view that when trying to match a signature against an actual argument tuple, each type parameter can always be bound to a type derived from one of the argument types, and then we check if the other constraints are fulfilled. I also see that this rule would minimize breakage while letting Still, I'm not quite comfortable with the special casing. It's easier that you unwittingly change a case from covariant to invariant or vice versa, and if you haven't read the manual very carefully, you might not even be aware of the distinction. Could we consider having
This would make it immediately clear to the reader that something different is going on. |
I have to admit that I'm not entirely comfortable with the special casing either, although I realize we've been doing something similar for a rather long time. Not sure how else to handle it, although I would mention that if you can dispatch on type paremeters being concrete, then covariance would be a good choice. |
@JeffBezanson @jakebolewski and I have had a followup discussion and we feel that |
Thinking about this some more, I realize that it is mostly an issue of consistency rather than functionality. Looking at the prototypical case where a type parameter appears only in covariant position,
the covariant behavior can be achieved anyway with one of
depending on how you want the matching of We would of course still permit type parameters appearing only once covariantly such as in When it comes to functionality, the only thing it seems that we stand to lose with the rule is the possibility to let e.g. |
One possibility for marking a type parameter as concrete might be to allow something like
I have no idea how much havoc it would wreak in the type system to have all concrete types and no others be instances of It appears that allowing One thing to note is that |
It's not just a question of whether Instead of arguing via covariance or invariance, I would describe Julia's current type matching rule as "match the (implicitly concrete) argument types against the types in the function signature, and deduce type parameters from that". Nothing in type matching is currently looking for supertypes (or unions) of two types to make things match. Having said this, which is obviously not news to you: @JeffBezanson, are you looking for a way to introduce such a behaviour (automatically looking for supertypes or unions)? If so, what about allowing type arithmetic in function signatures? Or maybe it would be better to extend type pattern descriptors? What would be the advantage of deducing supertypes or unions? Would it be to prevent specialization of routines? If so, couldn't that be expressed differently, with metadata instead of through the type system, or by analyzing the function body to see whether specialization is beneficial? |
The question is whether a tuple of actual argument types is a subtype of the method signature, so this does reduce to a question of covariance vs. invariance (the argument tuple is covariant). There wouldn't be any advantage to deducing supertypes or unions; we don't want that. However theory requires them, because |
Thanks for the rationale. I agree with @toivoh: It would then be necessary to express the notion " |
Hmm, yes, this may not be invariance exactly, since the intersection of With covariance, the intersection of |
I'm not sure that I understand the example. For any types I'm not sure how type intersections relate to the problem at hand. As I understand it, what we are discussing is
I guess that the tuple |
Yes, I should have said Type intersection relates because it has to be consistent with how method calls behave. Things will break if method calls add in extra behaviors that the rest of the type system doesn't know about. The question is how to conclude |
One way to think about this, which is what kind of makes them intuitive is that you can work backwards very concretely from actual argument types in a straightforward way – if |
That's true, this is not really quantification. For one thing, subtyping |
About the case with a type parameter |
I think that we should make the constraint that Btw, regarding the possibility for |
I don't think that the user cares whether a type Maybe one should treat matching and compiling as two separate steps. During matching it does not matter whether a type is concrete; as @toivoh says, the most generic matching type signature could be used. To compiler, the types are then specialized. This is a heuristic, and exactly how a routine is specialized does not influence correctness. In principle, the compiler could even specialize on the (inferred) type of local variables, i.e. introduce branches to keep values unboxed. |
Doesn't it, though? We want |
I stand corrected. |
Ref: discussion about negations in 23b6a1f |
Some thoughts on direct field access (#1974 #2614 #7561): From a practical perspective, languages differ on how they restrict field access:
In Julia we have no true privacy, but afaict we don't have explicit naming conventions or mangling mechanisms either. From a theoretical perspective, the fact that all fields are semantically public has consequences for the type system:
Julia uses nominative typing, where subtyping relations are explicitly defined. But there are cases where using nominal typing is problematic, causing users to use duck typing instead. Two examples I have in mind:
Is it a coincidence that many of the problematic cases encountered involve |
When this gets picked up again, one thing worth mentioning: lately, I have been wondering whether a good way to fix many problems with compilation time might be "abstract type inference." The idea would be that when possible, you express the result of type inference on a method as a set of For example, in running inference on getindex{T,N}(A::Array{T,N}, i) = arrayref(A, i) you might like to express the return value as In a more complicated case like function (+){S,T,N}(::A::Array{S,N}, B::Array{T,N})
R = promote_op(+, S, T)
# check sizes...
out = Array{R}(size(A))
for I in ...
end
out
end then you'd probably like I mention this here because this seems to require the ability to express a type in terms of functions. EDIT: if this could be pulled off, if nothing else it seems likely to speed the test suite: some of the slowest tests, e.g. subarray and the linalg suite, are slow precisely because they run the same methods with many different input types. |
Having read through this thread, I'm still not 100% sure of the desired behavior. I got:
But
|
If the second matches, I don't see how the third could not match (although I'm not sold on this behavior). |
I definitely think the second shouldn't match. I like the version in #8974 (comment), and it seems Jeff does too. |
Ah, yes, sorry, I had missed #8974 (comment), my bad. Makes a lot more sense that way. Carry on. |
So will this improve Union type performance? |
I came across this recently while using a lot of nested types: I'm not sure if it's something that you're planning on handling or not, but it might be a useful test case: julia> immutable Foo{T}
x::T
end
julia> immutable Bar{T} end
julia> typealias FooBar{T} Foo{Bar{T}}
Foo{Bar{T}}
julia> FooBar{Float64} <: FooBar
false It's been a minor annoyance, as I have to write |
Yes, the new subtyping algorithm should fix this. |
closed by #18457 |
@jakebolewski, @jiahao and I have been thinking through some type system improvements and things are starting to materialize. I believe this will consist of:
DataType
to implement tuple types, which will allow for more efficient tuples, and make tuple types less of a special case in various places. This might also open the door for user-defined covariant types in the future. (DONE)UnionAll
types around all uses of TypeVars. EachUnionAll
will bind one TypeVar, and we will nest them to allow for triangular dispatch. So far we've experimented with just sticking TypeVars wherever, and it has not really worked. We don't have a great syntax for this yet, but for now will probably use@UnionAll T<:Int Array{T}
. These are mostly equivalent to existential types.Complex
will be bound to@UnionAll T<:Real _Complex{T}
where_Complex
is an internal type constructor.(Int,String)
could be a subtype of@UnionAll T (T,T)
ifT
wereUnion(Int,String)
, orAny
. However dispatch doesn't work this way because it's not very useful. We effectively have a rule that if a method parameterT
only appears in covariant position, it ranges over only concrete types. We should apply this rule explicitly by marking TypeVars as appropriate. So far this is the best way I can think of to model this behavior, but suggestions are sought.@UnionAll
should suffice.This is being prototyped in
examples/juliatypes.jl
. I hope this will fix most of the following issues:method lookup and sorting issues:
#8959 #8915 #7221 #8163
method ambiguity issues:
#8652 #8045 #6383 #6190 #6187 #6048 #5460 #5384 #3025 #1631 #270
misc. type system:
#8920 #8625 #6984 #6721 #3738 #2552 #8470
In particular, type intersection needs to be trustworthy enough that we can remove the ambiguity warning and generate a runtime error instead.
The text was updated successfully, but these errors were encountered: