# Possible bug with dispatch on parametric abstract type

**URL:** <https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852>\
**Category:** General Usage\
**Created:** [May 14, 2023, 10:30pm UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852 "2023-05-14T22:30:44Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![bgroenks](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bgroenks/32/21784_2.png) [@bgroenks](https://discourse.julialang.org/u/bgroenks)\
**Post date:** [May 14, 2023, 10:30pm UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852/1 "2023-05-14T22:30:44Z")

</div>

Hi all,

I think I may have encountered a possible bug in Julia’s method dispatch. But I would like to post this here first in case anyone can spot something that I have missed. I was able to reproduce the problem in a simple MWE modeled after my actual use case:

```julia
abstract type ParamType end
struct ParamImpl <: ParamType end
struct Foo end
struct Bar end
abstract type TestType{T1<:ParamType,T2<:Union{Nothing,<:Foo},T3<:Union{Nothing,<:Bar}} end
Base.@kwdef struct TestTypeImpl{T1,T2,T3} <: TestType{T1,T2,T3}
    val1::T1 = ParamImpl()
    val2::T2 = Foo()
    val3::T3 = nothing
end
testfunc(::TestType{<:ParamImpl}, x) = x
testfunc(::TestType{<:ParamImpl,T2,<:Bar}, x) where {T2} = x*x

testfunc(TestTypeImpl(val3=Bar()), 2.0)
@which testfunc(TestTypeImpl(val3=Bar()), 2.0)
@assert typeof(TestTypeImpl(val3=Bar())) <: TestType{<:ParamImpl,T2,<:Bar} where {T2}

```

The call to `testfunc` then invokes the wrong method:

```julia
julia> testfunc(TestTypeImpl(val3=Bar()), 2.0)
2.0

```

The expected behavior here is that the second method of `testfunc` should be called, as the assertion proves that this is the correct and more specific dispatch. I haven’t been able to narrow down yet exactly what breaks this, but I’d like to get a second opinion before I spend more time on it.

Julia version info:

```julia
Julia Version 1.8.5
Commit 17cfb8e65ea (2023-01-08 06:45 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-13.0.1 (ORCJIT, skylake)
  Threads: 1 on 8 virtual cores

```

It appears to happen on v1.9 as well.

Thanks!

---

<div class="post-metadata">

**Author:** ![odow](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/odow/32/28685_2.png) [@odow](https://discourse.julialang.org/u/odow)\
**Post date:** [May 14, 2023, 11:50pm UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852/2 "2023-05-14T23:50:14Z")

</div>

So here’s a minimal example of what’s going on;

```julia
julia> struct A end

julia> struct B end

julia> struct C{T1<:A,T2<:B} end

julia> main(::C) = 1
main (generic function with 1 method)

julia> main(::C{T,<:B}) where {T} = 2
main (generic function with 2 methods)

julia> @assert main(C{A,B}()) == 2
ERROR: AssertionError: main(C{A, B}()) == 2

```

Expanding out the `where {T}` part and the inferred type parameters for the `1` method gives:

```julia
julia> struct A end

julia> struct B end

julia> struct C{T1<:A,T2<:B} end

julia> main(::C{<:A,<:B}) = 1
main (generic function with 1 method)

julia> main(::C{<:Any,<:B}) = 2
main (generic function with 2 methods)

julia> @assert main(C{A,B}()) == 2
ERROR: AssertionError: main(C{A, B}()) == 2

```

So this show’s what is going on. Method 2 really is less-specific than method 1, and so method 1 gets called.

To fix your method, you need to change

```julia
testfunc(::TestType{<:ParamImpl,T2,<:Bar}, x) where {T2} = x*x
# to
testfunc(::TestType{<:ParamImpl,T2,<:Bar}, x) where {T2<:Union{Nothing,<:Foo}} = x*x

```

```plaintext
julia> abstract type ParamType end

julia> struct ParamImpl <: ParamType end

julia> struct Foo end

julia> struct Bar end

julia> abstract type TestType{T1<:ParamType,T2<:Union{Nothing,<:Foo},T3<:Union{Nothing,<:Bar}} end

julia> Base.@kwdef struct TestTypeImpl{T1,T2,T3} <: TestType{T1,T2,T3}
           val1::T1 = ParamImpl()
           val2::T2 = Foo()
           val3::T3 = nothing
       end

julia> testfunc(::TestType{<:ParamImpl}, x) = x
testfunc (generic function with 1 method)

julia> testfunc(::TestType{<:ParamImpl,T2,<:Bar}, x) where {T2<:Union{Nothing,<:Foo}} = x*x
testfunc (generic function with 2 methods)

julia> testfunc(TestTypeImpl(val3=Bar()), 2.0)
4.0

```

---

<div class="post-metadata">

**Author:** ![bgroenks](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bgroenks/32/21784_2.png) [@bgroenks](https://discourse.julialang.org/u/bgroenks)\
**Post date:** [May 15, 2023, 5:46am UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852/3 "2023-05-15T05:46:37Z")

</div>

Thanks!

This, however:

```julia
julia> main(::C{<:A,<:B}) = 1
main (generic function with 1 method)

julia> main(::C{<:Any,<:B}) = 2
main (generic function with 2 methods)

```

is very surprising to me. I always assumed that Julia always used the relevant upper bound on the parametric type. I don’t really understand why it does so in the first case but not in the second?

For the first method, `::C` is a `UnionAll` type that implicitly expands to `::C{T1,T2} where {T1,T2}`; so logically, per your explanation regarding the second method, this should also be then further expanded to `::C{T1,T2} where {T1<:Any,T2<:Any}` but it’s not. Why is this?

---

<div class="post-metadata">

**Author:** ![Sevi](https://avatars.discourse-cdn.com/v4/letter/s/c67d28/32.png) [@Sevi](https://discourse.julialang.org/u/Sevi)\
**Post date:** [May 15, 2023, 6:23am UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852/4 "2023-05-15T06:23:35Z")

</div>

> [@bgroenks](#):
>
> For the first method, `::C` is a `UnionAll` type that implicitly expands to `::C{T1,T2} where {T1,T2}`;

I don’t think this is true. The definition of `C` restricts both parameters to be subtypes of `A` and `B` respectively, which is more specific than allowing subtypes of `Any` and `B`. E.g. this doesn’t work:

```julia
julia> C{Int, Float64}
ERROR: TypeError: in C, in T1, expected T1<:A, got Type{Int64}

```

And since `A` and `B` are concrete types, really only those types are allowed as parameters for `C` and a method that accepts some `C{<:Any, <:B}` won’t ever be called in such a situation, because no such objects will exist (exept `C{A,B}`).

---

<div class="post-metadata">

**Author:** ![bgroenks](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bgroenks/32/21784_2.png) [@bgroenks](https://discourse.julialang.org/u/bgroenks)\
**Post date:** [May 15, 2023, 8:39am UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852/5 "2023-05-15T08:39:31Z")

</div>

> And since `A` and `B` are concrete types, really only those types are allowed as parameters for `C` and a method that accepts some `C{<:Any, <:B}` won’t ever be called in such a situation, because no such objects will exist (exept `C{A,B}`).

Yes, and that’s the point that I am trying to make. When expanding the method `main(::C{T,<:B}) where {T}`, it strikes me as erroneous behavior for Julia to use `T<:Any` as the upper bound for `T` because the type parameters of `C` are bounded such that this type cannot exist. I don’t see any reason why the type bound for `T` should not be automatically inferred from the type parameters of the struct… but maybe there is some internal technical reason why this isn’t possible?

---

<div class="post-metadata">

**Author:** ![sijo](https://avatars.discourse-cdn.com/v4/letter/s/da6949/32.png) [@sijo](https://discourse.julialang.org/u/sijo)\
**Post date:** [May 15, 2023, 11:23am UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852/6 "2023-05-15T11:23:05Z")

</div>

This also looks odd to me:

```julia
struct S{T<:Number}
end

julia> S{AbstractString}
ERROR: TypeError: in S, in T, expected T<:Number, got Type{AbstractString}

julia> S{<:AbstractString}
S{<:AbstractString}

```

---

<div class="post-metadata">

**Author:** ![Sevi](https://avatars.discourse-cdn.com/v4/letter/s/c67d28/32.png) [@Sevi](https://discourse.julialang.org/u/Sevi)\
**Post date:** [May 15, 2023, 12:38pm UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852/7 "2023-05-15T12:38:56Z")

</div>

Ah, then I misunderstood your point. I thought you said that in `main(::C)` the `C` is treated as `C{T1, T2} where {T1, T2}`, but it is treated just as `C{<:A,<:B}` as it is defined.

* * *

I don’t know enough about the internals to say if or why it’s not possible to restrict method definitions for types unions that cannot have any instances, but it would feel weird to me if the type would automatically be restricted, e.g. if these two would always match the same signature:

```julia
main(::C{<:A,<:B})
main(::C{<:Any,<:B})

```

Defining the second would then overwrite the first, but semantically I read this as “do the first method if you have `C{A,B}` and the second if you have any other `C{...,B}`”.

I guess it would be nice if there could be at least a warning when one tries to define methods for empty type unions, like in the example from @sijo .

---

<div class="post-metadata">

**Author:** ![Sevi](https://avatars.discourse-cdn.com/v4/letter/s/c67d28/32.png) [@Sevi](https://discourse.julialang.org/u/Sevi)\
**Post date:** [May 15, 2023, 12:49pm UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852/8 "2023-05-15T12:49:08Z")

</div>

Yeah, it really looks odd. My personal interpretation is that with `S{AbstractString}` we are trying to talk about a type which has no instances (i.e. is useless?), whereas `S{<:AbstractString}` is a union of types which just happens to be empty (a little bit like `Union{}`, meaning, of course, that it also has no possible instances… but an empty type union is used at other places in Julia I believe).

As a (probably bad) analogy: I can create an array with no elements `[x for x in 1:0]`, but I cannot create an object that is just “empty”.

---

<div class="post-metadata">

**Author:** ![bgroenks](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bgroenks/32/21784_2.png) [@bgroenks](https://discourse.julialang.org/u/bgroenks)\
**Post date:** [May 15, 2023, 12:52pm UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852/9 "2023-05-15T12:52:25Z")

</div>

> [@Sevi](#):
>
> Ah, then I misunderstood your point. I thought you said that in `main(::C)` the `C` is treated as `C{T1, T2} where {T1, T2}`, but it is treated just as `C{<:A,<:B}` as it is defined.

Right, I mean that this is what it _should_ expand to in order to be consistent with the behavior in the other case. I realize that this is not what actually happens. I am trying to understand the apparent inconsistency.

> [@Sevi](#):
>
> e.g. if these two would always match the same signature:

But I think the point is that Julia should not let you define the second one because it is not a valid type… or rather it has an invalid upper bound.

If you write `A{T}` and `A` has a bounded type parameter then `T` should also implicitly have this bound.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [May 15, 2023, 8:31pm UTC](https://discourse.julialang.org/t/possible-bug-with-dispatch-on-parametric-abstract-type/98852/10 "2023-05-15T20:31:08Z")

</div>

> [@sijo](#):
>
> This also looks odd to me:

It’s just replacing a parametric type’s `where` clause, in this case `struct S{T<:Number}` with `where T<: AbstractString`. Replacing `where` clauses is routine, obviously useful for specifying abstract subtypes, like `Ref{T} where T<:Integer`. But sometimes you don’t want to write types as strictly for easier editing, or you need nonspecific type bounds in the `where` clause:

```julia
# can edit the union and where clauses afterward
MyTypes = Union{Number, AbstractString}
struct S{T<:Number} end
struct R{T<:AbstractString} end

# and leave the methods as is
bar(a::S{T} where T<:MyTypes, b::R{U} where U<:MyTypes) = 1

# type bound for T is an unknown U, even though
# Vector's where is less constrained than S's
foo(a::Vector{T}, b::S{U}) where {U, T<:U} = 0

```

I assume this is the reason that `where` clauses aren’t checked against their original `struct` immediately.

> [@bgroenks](#):
>
> But I think the point is that Julia should not let you define the second one because it is not a valid type… or rather it has an invalid upper bound.

I am leaning toward agreeing that Julia should check `<:DataType` bounds in `where` clauses because an error is thrown if you attempt to make any subtype of `S{<:AbstractString}`, but someone feel free to show me if this is unfeasible or if an impossible type can be useful.
