Unexpected result from ==

A dictionary in my code is not working because keys that are supposed to be equal are comparing as unequal. Here is a snippet (Julia 1.8.2). Is this a bug, or am I misunderstanding how == is supposed to work?

julia> abstract type ET end

julia> struct ET1 <: ET
       end

julia> struct SK1{T <: ET}
       nl::Vector{Int}
       end

julia> k1 = SK1{ET1}([4,3,5])
SK1{ET1}([4, 3, 5])

julia> k2 = SK1{ET1}([4,3,5])
SK1{ET1}([4, 3, 5])

julia> k1 == k2
false

== on structs by default calls === on it’s members. If you want different behavior, you need to overload ==.

3 Likes

And to expand on oscars answer, this is what the docstring for === says

help?> ===
search: === == !==

  ===(x,y) -> Bool
  ≡(x,y) -> Bool


  Determine whether x and y are identical, in the sense that no program could
  distinguish them. First the types of x and y are compared. If those are identical,
  mutable objects are compared by address in memory and immutable objects (such as
  numbers) are compared by contents at the bit level. This function is sometimes
  called "egal". It always returns a Bool value.

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> a = [1, 2]; b = [1, 2];

  julia> a == b
  true

  julia> a === b
  false

  julia> a === a
  true

so it makes sense since the objects are not the same, they just contain the same values.

If you want to check if they contain the same value, you could compare the content instead

k1.nl == k2.nl

or you could overload == for your type if want to directly compare them

Base.:(==)(a::SK1, b::SK1) = a.nl == b.nl
2 Likes

And, if you ever choose to make SK1 contain more fields:

Base.:(==)(a::SK1, b::SK1) = 
    all(getfield(a, s) == getfield(b, s) for s ∈ fieldnames(SK1))

Sorry, I’m not getting it yet; here are some followup questions.

  1. The first response from Oscar_Smith, which appears to correctly describe the behavior of Julia according to my tests, is inconsistent with the docstring. Here is an excerpt from the docstring:
help?> ==
   [snip]
For collections, == is generally
  called recursively on all contents, though other properties (like the shape for arrays) may also be taken into
  account.

So in other words, the docstring seems to say that == should be called on the nl field, not ===.

  1. My actual requirement is to use SK as a dict key, and I haven’t gotten this to work yet. Below is my attempt to make this work. I was expecting the output of the final four @show statements to be 8, 8, 9, 10, but instead I got 7,8,9,10 when I ran the code below, so I am still making some mistake.

Thanks

abstract type E end
import Base.==
import Base.hash

struct E1 <: E
end

struct E2 <: E
end

struct SK{T <: E}
    nl::Vector{Int}
end

getparamtype(sk::SK{T}) where T = T
==(sk1::SK, sk2::SK) = getparamtype(sk1) === getparamtype(sk2) && sk1.nl == sk2.nl
hash(sk::SK,h::Int) = hash((hash(getparamtype(sk),h),hash(sk.nl,h)),h) 


a = SK{E1}([4,3,5])
b = SK{E1}([4,3,5])
c = SK{E1}([4,3,4])
d = SK{E2}([4,3,5])

@show a == a
@show a == b
@show a == c
@show a == d
@show a != a
@show a != b
@show a != c
@show a != d

u = Dict{SK,Int}()
u[a] = 7
u[b] = 8
u[c] = 9
u[d] = 10
@show u[a]
@show u[b]
@show u[c]
@show u[d]

I think you want hash(sk::SK,h::UInt) = ..., then your example seems to work for me at least.

1 Like

Thanks! Changing Int to UInt indeed fixed the problem with setting up a Dict, which was my main issue.

Regarding my other question: is the docstring for == incorrect or am I misunderstanding it?

I don’t think it is wrong, but maybe the documentation could be made easier to understand?

Docstring of == mentions that it falls back to ===, which I believe is the case we will hit since even though the struct contains a collection the struct itself is not a collection.

Generic equality operator. Falls back to ===. Should be implemented for all types with a notion of
equality, based on the abstract value that an instance represents. For example, all numeric types are
compared by numeric value, ignoring type. Strings are compared as sequences of characters, ignoring
encoding. For collections, == is generally called recursively on all contents, though other
properties (like the shape for arrays) may also be taken into account.

Docstring of === mentions that immutable objects are compared by bit level contents (this is your struct) and immutable objects by pointers (this is the Vector inside your struct). So I think since the contents of you struct is mutable it will not be stored “inside” your struct, but your struct rather has a pointer to where on the heap those values are, resulting in the pointer being compared by default.

Determine whether x and y are identical, in the sense that no program could distinguish them. First
the types of x and y are compared. If those are identical, mutable objects are compared by address in
memory and immutable objects (such as numbers) are compared by contents at the bit level. This
function is sometimes called “egal”. It always returns a Bool value.

There’s an issue scheduled for 2.0 about this.

https://github.com/JuliaLang/julia/issues/4648

3 Likes