Skip to content
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

Dispatch pitfall due to Type{Union{}} <: Type{<:MyType}. #33780

Closed
tkf opened this issue Nov 7, 2019 · 7 comments · Fixed by #49349
Closed

Dispatch pitfall due to Type{Union{}} <: Type{<:MyType}. #33780

tkf opened this issue Nov 7, 2019 · 7 comments · Fixed by #49349
Labels
types and dispatch Types, subtyping and method dispatch

Comments

@tkf
Copy link
Member

tkf commented Nov 7, 2019

I found that there is a pitfall with dispatching on custom parameterized types due to Type{Union{}} <: Type{<:MyType} (JuliaArrays/StaticArrays.jl#685). Borrowing @c42f's example, the problem is that:

julia> foo(::Type{T}) where {T} = "generic"
julia> foo(::Type{<:Int}) = "special"
julia> foo(Union{})
"special"

For a concrete type like Int, the solution is to write Type{Int}. However, it is tricky for parameterized types. Possible workarounds are:

  1. Use foo(::Type{T}) where {P, MyType{P} <: T <: MyType}: However, this interferes with the inliner Unpirate Union{}[] JuliaArrays/StaticArrays.jl#685 (comment) possibly due to unbound type parameters (see also Remove some spurious where T from SA constructors JuliaArrays/StaticArrays.jl#665, lower bound interferes with typevar value inference #30713).

  2. Separate dispatches as in foo(::Type{MyType}) and foo(::Type{MyType{P}}) where P. Unpirate Union{}[] JuliaArrays/StaticArrays.jl#685 uses this solution.

  3. Write a helper function like Type₁(U) = Type{T} where {P, U{P} <: T <: U} and then define foo(::Type₁(MyType)).

However, none of the above solutions works well when there are many parameters. It may be useful to have a generalization of approach 3 (say) Type₊(U::UnionAll) that works with an arbitrary number of parameters. Or maybe there is a better solution?

@c42f
Copy link
Member

c42f commented May 14, 2020

Would it be consistent and possible to have an implicit open lower bound in foo(::Type{<:T}) where T, so that Union{} is excluded?

Generally it just doesn't seem very useful to allow Union in this circumstance, in fact I'd be extremely surprised if it were intended by the average user, and it's also related to of other problems such as method invalidation due to spurious ambiguities (mentioned in JuliaLang/www.julialang.org#794)

@tkf tkf added the types and dispatch Types, subtyping and method dispatch label May 15, 2020
@tkf
Copy link
Member Author

tkf commented May 15, 2020

It's not possible to do this in 1.x, right? Changing something fundamental like the syntax for talking to the types system sounds like impossible to argue that it is a "minor change" while I do agree that most use-cases of Type{<:MyType} are not aware the possibility of Union{}.

But can we still introduce some open lower bound syntax? Type{T} where {Union{} <<: T <: MyType}?

Somewhat related?: Explicit concrete type constraint in UnionAll #30363

@tkf
Copy link
Member Author

tkf commented May 15, 2020

By the way, here is another surprising behavior:

julia> Base.promote_rule(Union{}, Int)
Float64

julia> @which Base.promote_rule(Union{}, Int)
promote_rule(::Type{var"#s822"} where var"#s822"<:AbstractIrrational, ::Type{T}) where T<:Real in Base at irrationals.jl:42

Though promote_type still works since it explicitly handles Union{}:

julia> promote_type(Union{}, Int)
Int64

@c42f
Copy link
Member

c42f commented May 15, 2020

It's not possible to do this in 1.x, right?

While changing the syntax is impossible, I thought it might be possible to subtly change the of the "no lower bound" case to exclude Union{} from ::Type{<:T}? It's certainly an incompatible change in principle but removing a rather surprising edge case might very well fix more code than it breaks. In that case, is it a breaking change? I don't know :-)

@tkf
Copy link
Member Author

tkf commented May 15, 2020

I'd say, strictly speaking, it is a breaking change if it changes the behavior of any code that only depends on defined behaviors. Yes, I know that's too strict and also most of the API is not defined enough anyway. IIUC that's why we have "minor change" as an escape hatch. But I don't think even "minor change" is appropriate for something fundamental like the type system.

@c42f
Copy link
Member

c42f commented May 15, 2020

Well I certainly don't have knowledge of the type system theory or implementation to judge what havoc this idea would cause ;-)

I'm more wondering whether it's justified from a user perspective. My guess is that when people write foo(::Type{<:T}) it's "never" their intention to include Union{}, so maybe we'd get away with changing the meaning.

@tkf
Copy link
Member Author

tkf commented May 15, 2020

I think it's cloes to the 0.6 scoping on REPL. No one writes for x in xs; s += x; end for throwing UndefVarError.

I think it's reasonable to asume that there is code like

struct TypeContainer{T}
    x::Type{<:T}
end

which is called via TypeContainer{Real}(Union{}). Breaking this code is a breaking change. This code looks so innocent that I can't say it's even a "minor change."

Adding a new syntax/API sounds like a better solution to me.

vtjnash added a commit that referenced this issue Apr 13, 2023
Based on the new morespecific rule for Union{} and method definitions of
the specific form `f(..., Type{Union{}}, Vararg)`. If a method
definition exists with that specific form, the intersection visitor will
ignore all intersections that have that as their only result, saving
significant effort when working with lookups involving `Type{<:T}`
(which usually ended up mostly ambiguous anyways).

Fixes: #33780

This pattern turns out to have still to been making package loading
slow. We could keep adding methods following the ambiguity pattern
#46000 for the couple specific
functions that need it (constructor, eltype, IteratorEltype,
IteratorSize, and maybe a couple others) so the internals can detect
those and optimize functions that have that method pair. But it seems
somewhat odd, convoluted, and non-obvious behavior there. Instead, this
breaks all ambiguities in which Union{} is present explicitly in favor
of the method with Union{}. This means that when computing method
matches, as soon as we see one method definition with Union{}, we can
record that the method is the only possible match for that slot.

This, in essence, permits creating a rule for dispatch that a TypeVar
lower bound must be strictly a supertype of Union{}, but this creates it
at the function level, instead of expecting the user to add it to every
TypeVar they use to define methods.

This also lets us improve the error message for these cases (generally
they should error to avoid polluting the inference result), since we can
be assured this method will be called, and not result in an ambiguous
MethodError instead!

Reverts the functional change of #46000
vtjnash added a commit that referenced this issue Apr 13, 2023
Based on the new morespecific rule for Union{} and method definitions of
the specific form `f(..., Type{Union{}}, Vararg)`. If a method
definition exists with that specific form, the intersection visitor will
ignore all intersections that have that as their only result, saving
significant effort when working with lookups involving `Type{<:T}`
(which usually ended up mostly ambiguous anyways).

Fixes: #33780

This pattern turns out to have still to been making package loading
slow. We could keep adding methods following the ambiguity pattern
#46000 for the couple specific
functions that need it (constructor, eltype, IteratorEltype,
IteratorSize, and maybe a couple others) so the internals can detect
those and optimize functions that have that method pair. But it seems
somewhat odd, convoluted, and non-obvious behavior there. Instead, this
breaks all ambiguities in which Union{} is present explicitly in favor
of the method with Union{}. This means that when computing method
matches, as soon as we see one method definition with Union{}, we can
record that the method is the only possible match for that slot.

This, in essence, permits creating a rule for dispatch that a TypeVar
lower bound must be strictly a supertype of Union{}, but this creates it
at the function level, instead of expecting the user to add it to every
TypeVar they use to define methods.

This also lets us improve the error message for these cases (generally
they should error to avoid polluting the inference result), since we can
be assured this method will be called, and not result in an ambiguous
MethodError instead!

Reverts the functional change of #46000
vtjnash added a commit that referenced this issue Apr 13, 2023
Based on the new morespecific rule for Union{} and method definitions of
the specific form `f(..., Type{Union{}}, Vararg)`. If a method
definition exists with that specific form, the intersection visitor will
ignore all intersections that have that as their only result, saving
significant effort when working with lookups involving `Type{<:T}`
(which usually ended up mostly ambiguous anyways).

Fixes: #33780

This pattern turns out to have still to been making package loading
slow. We could keep adding methods following the ambiguity pattern
#46000 for the couple specific
functions that need it (constructor, eltype, IteratorEltype,
IteratorSize, and maybe a couple others) so the internals can detect
those and optimize functions that have that method pair. But it seems
somewhat odd, convoluted, and non-obvious behavior there. Instead, this
breaks all ambiguities in which Union{} is present explicitly in favor
of the method with Union{}. This means that when computing method
matches, as soon as we see one method definition with Union{}, we can
record that the method is the only possible match for that slot.

This, in essence, permits creating a rule for dispatch that a TypeVar
lower bound must be strictly a supertype of Union{}, but this creates it
at the function level, instead of expecting the user to add it to every
TypeVar they use to define methods.

This also lets us improve the error message for these cases (generally
they should error to avoid polluting the inference result), since we can
be assured this method will be called, and not result in an ambiguous
MethodError instead!

Reverts the functional change of #46000
vtjnash added a commit that referenced this issue Apr 18, 2023
Based on the new morespecific rule for Union{} and method definitions of
the specific form `f(..., Type{Union{}}, Vararg)`. If a method
definition exists with that specific form, the intersection visitor will
ignore all intersections that have that as their only result, saving
significant effort when working with lookups involving `Type{<:T}`
(which usually ended up mostly ambiguous anyways).

Fixes: #33780

This pattern turns out to have still to been making package loading
slow. We could keep adding methods following the ambiguity pattern
#46000 for the couple specific
functions that need it (constructor, eltype, IteratorEltype,
IteratorSize, and maybe a couple others) so the internals can detect
those and optimize functions that have that method pair. But it seems
somewhat odd, convoluted, and non-obvious behavior there. Instead, this
breaks all ambiguities in which Union{} is present explicitly in favor
of the method with Union{}. This means that when computing method
matches, as soon as we see one method definition with Union{}, we can
record that the method is the only possible match for that slot.

This, in essence, permits creating a rule for dispatch that a TypeVar
lower bound must be strictly a supertype of Union{}, but this creates it
at the function level, instead of expecting the user to add it to every
TypeVar they use to define methods.

This also lets us improve the error message for these cases (generally
they should error to avoid polluting the inference result), since we can
be assured this method will be called, and not result in an ambiguous
MethodError instead!

Reverts the functional change of #46000
vtjnash added a commit that referenced this issue Apr 23, 2023
Based on the new morespecific rule for Union{} and method definitions of
the specific form `f(..., Type{Union{}}, Vararg)`. If a method
definition exists with that specific form, the intersection visitor will
ignore all intersections that have that as their only result, saving
significant effort when working with lookups involving `Type{<:T}`
(which usually ended up mostly ambiguous anyways).

Fixes: #33780

This pattern turns out to have still to been making package loading
slow. We could keep adding methods following the ambiguity pattern
#46000 for the couple specific
functions that need it (constructor, eltype, IteratorEltype,
IteratorSize, and maybe a couple others) so the internals can detect
those and optimize functions that have that method pair. But it seems
somewhat odd, convoluted, and non-obvious behavior there. Instead, this
breaks all ambiguities in which Union{} is present explicitly in favor
of the method with Union{}. This means that when computing method
matches, as soon as we see one method definition with Union{}, we can
record that the method is the only possible match for that slot.

This, in essence, permits creating a rule for dispatch that a TypeVar
lower bound must be strictly a supertype of Union{}, but this creates it
at the function level, instead of expecting the user to add it to every
TypeVar they use to define methods.

This also lets us improve the error message for these cases (generally
they should error to avoid polluting the inference result), since we can
be assured this method will be called, and not result in an ambiguous
MethodError instead!

Reverts the functional change of #46000
vtjnash added a commit that referenced this issue Apr 24, 2023
Based on the new morespecific rule for Union{} and method definitions of
the specific form `f(..., Type{Union{}}, Vararg)`. If a method
definition exists with that specific form, the intersection visitor will
ignore all intersections that have that as their only result, saving
significant effort when working with lookups involving `Type{<:T}`
(which usually ended up mostly ambiguous anyways).

Fixes: #33780

This pattern turns out to have still to been making package loading
slow. We could keep adding methods following the ambiguity pattern
#46000 for the couple specific
functions that need it (constructor, eltype, IteratorEltype,
IteratorSize, and maybe a couple others) so the internals can detect
those and optimize functions that have that method pair. But it seems
somewhat odd, convoluted, and non-obvious behavior there. Instead, this
breaks all ambiguities in which Union{} is present explicitly in favor
of the method with Union{}. This means that when computing method
matches, as soon as we see one method definition with Union{}, we can
record that the method is the only possible match for that slot.

This, in essence, permits creating a rule for dispatch that a TypeVar
lower bound must be strictly a supertype of Union{}, but this creates it
at the function level, instead of expecting the user to add it to every
TypeVar they use to define methods.

This also lets us improve the error message for these cases (generally
they should error to avoid polluting the inference result), since we can
be assured this method will be called, and not result in an ambiguous
MethodError instead!

Reverts the functional change of #46000
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
types and dispatch Types, subtyping and method dispatch
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants