Skip to content

defining a method of a generic comparison with one argument unconstrained in a package is a bug #310

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

Open
nsajko opened this issue Apr 3, 2025 · 5 comments

Comments

@nsajko
Copy link
Contributor

nsajko commented Apr 3, 2025

Defining a == method with one of the arguments untyped should never be done in a registered package. This is done here, and in some other places:

Base.:(==)(α, q::RationalPoly) = α * q.den == q.num
Base.:(==)(q::RationalPoly, α) = α == q

The reason that's a bug is that that kind of method signature will be ambiguous with regards to methods defined in other packages. Observe, fresh REPL session:

julia> struct A end  # in one package

julia> struct B end  # in another package

julia> function Base.:(==)(::A, ::Any) end

julia> function Base.:(==)(::Any, ::A) end

julia> function Base.:(==)(::B, ::Any) end

julia> function Base.:(==)(::Any, ::B) end

julia> using Test

julia> detect_ambiguities(Main)
12-element Vector{Tuple{Method, Method}}:
 (==(::A, ::Any) @ Main REPL[3]:1, ==(::Any, ::Missing) @ Base missing.jl:76)
 (==(::Any, ::A) @ Main REPL[4]:1, ==(::Missing, ::Any) @ Base missing.jl:75)
 (==(::A, ::Any) @ Main REPL[3]:1, ==(w, v::WeakRef) @ Base gcutils.jl:36)
 (==(::Any, ::A) @ Main REPL[4]:1, ==(w::WeakRef, v) @ Base gcutils.jl:35)
 (==(::A, ::Any) @ Main REPL[3]:1, ==(::Any, ::B) @ Main REPL[6]:1)
 (==(::A, ::Any) @ Main REPL[3]:1, ==(::Any, ::A) @ Main REPL[4]:1)
 (==(::B, ::Any) @ Main REPL[5]:1, ==(::Any, ::B) @ Main REPL[6]:1)
 (==(::Any, ::B) @ Main REPL[6]:1, ==(::Missing, ::Any) @ Base missing.jl:75)
 (==(::B, ::Any) @ Main REPL[5]:1, ==(w, v::WeakRef) @ Base gcutils.jl:36)
 (==(::B, ::Any) @ Main REPL[5]:1, ==(::Any, ::Missing) @ Base missing.jl:76)
 (==(::Any, ::A) @ Main REPL[4]:1, ==(::B, ::Any) @ Main REPL[5]:1)
 (==(::Any, ::B) @ Main REPL[6]:1, ==(w::WeakRef, v) @ Base gcutils.jl:35)

Or, more minimally and concretely:

julia> struct A end  # in one package

julia> function Base.:(==)(::A, ::Any) end

julia> struct B end  # in another package

julia> function Base.:(==)(::Any, ::B) end

julia> A() == B()
ERROR: MethodError: ==(::A, ::B) is ambiguous.

Candidates:
  ==(::A, ::Any)
    @ Main REPL[2]:1
  ==(::Any, ::B)
    @ Main REPL[4]:1

Possible fix, define
  ==(::A, ::B)

Stacktrace:
 [1] top-level scope
   @ REPL[5]:1

I saw this on issue #290 first, but decided to open a new issue, since what I'm pointing out seems worse than invalidation of precompiled code and is not related to invalidation.

@nsajko nsajko changed the title defining equality with one argument unconstrained in a package is a bug defining a comparison method with one argument unconstrained in a package is a bug Apr 3, 2025
@nsajko nsajko changed the title defining a comparison method with one argument unconstrained in a package is a bug defining a method of a generic comparison with one argument unconstrained in a package is a bug Apr 3, 2025
@blegat
Copy link
Member

blegat commented Apr 3, 2025

The strategy I use for most operators (not only ==) is to define op(::Any, ::A), op(::A, ::Any) and op(::A, ::A) then you don't have any ambiguity. If several types are defined in the package you use A being the union of all of them. Then you redirect the methods that received Any to a method op_constant to avoid any issues with ambiguities. So in principle 3 methods are enough.

@nsajko
Copy link
Contributor Author

nsajko commented Apr 3, 2025

define op(::Any, ::A), op(::A, ::Any) and op(::A, ::A) then you don't have any ambiguity

This is not true. The extra method obviously has no effect on the examples I give above.

@nsajko
Copy link
Contributor Author

nsajko commented Apr 3, 2025

To be specific:

julia> struct A end  # in one package

julia> function Base.:(==)(::A, ::A) end

julia> function Base.:(==)(::Any, ::A) end

julia> function Base.:(==)(::A, ::Any) end

julia> struct B end  # in another package

julia> function Base.:(==)(::B, ::B) end

julia> function Base.:(==)(::Any, ::B) end

julia> function Base.:(==)(::B, ::Any) end

julia> A() == B()
ERROR: MethodError: ==(::A, ::B) is ambiguous.

Candidates:
  ==(::A, ::Any)
    @ Main REPL[4]:1
  ==(::Any, ::B)
    @ Main REPL[7]:1

Possible fix, define
  ==(::A, ::B)

Stacktrace:
 [1] top-level scope
   @ REPL[9]:1

@blegat
Copy link
Member

blegat commented Apr 3, 2025

Sorry I didn't read in detail, yes indeed, it won't work of two packages do it. Would the suggestion in #290 (comment) address the concern ?

nsajko added a commit to nsajko/julia that referenced this issue Apr 3, 2025
The fact that an important (for the ecosystem) registered package
failed to follow the advice/rule provided in the change is evidence to
the claim that adding these docs is important. Ref:
* JuliaAlgebra/MultivariatePolynomials.jl#310

Fixes JuliaLang#55866
@nsajko
Copy link
Contributor Author

nsajko commented Apr 3, 2025

FYI: created a doc PR for JuliaLang/julia regarding a generalization of the issue here, to warn against it in the Manual:

Perhaps take a look at it if there's time, it could be good to review if something is not clear or otherwise.

Would the suggestion in #290 (comment) address the concern ?

I believe so, yeah.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants