JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.72k stars 5.48k forks source link

Comparisons of structs with == lower suprisingly to === and documenation doesn't make it clear #51133

Open freemin7 opened 1 year ago

freemin7 commented 1 year ago
versioninfo()
Julia Version 1.8.5
Commit 17cfb8e65ea (2023-01-08 06:45 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-13.0.1 (ORCJIT, tigerlake)
  Threads: 1 on 8 virtual cores

installed via JuliaUp

using UUIDs

struct flower
    car::Vector{UUID}
    house::UUID
end

 uidv = [UUIDs.uuid4(), UUIDs.uuid4(), UUIDs.uuid4()]
 uid = UUIDs.uuid4()

 lettuce = flower(uidv, uid)

@assert typeof(lettuce) == typeof(eval( Meta.parse( string(flower(uidv, uid))))) ## passes
@assert lettuce.car == eval( Meta.parse( string(flower(uidv, uid)))).car ## passes
@assert typeof(lettuce.car) == typeof(eval( Meta.parse( string(flower(uidv, uid)))).car) ## passes
@assert lettuce.house == eval( Meta.parse( string(flower(uidv, uid)))).house ## passes
@assert typeof(lettuce.house) == typeof(eval( Meta.parse( string(flower(uidv, uid)))).house) ## passes
@assert fieldcount(flower) == 2
@assert string(lettuce) == string(eval( Meta.parse( string(flower(uidv, uid))))) ## passess

@assert hash(lettuce.house) == hash(eval( Meta.parse( string(flower(uidv, uid)))).house) ## passes
@assert hash(lettuce.car) == hash(eval( Meta.parse( string(flower(uidv, uid)))).car) ## passes
@assert flower(uidv, uid) == flower(uidv, uid)

@assert flower(uidv, uid) == eval(:(flower(uidv, uid)))

## So they should be identical in every way but
## both comparisons below fail
@assert lettuce == eval( Meta.parse( string(flower(uidv, uid)))) 
@assert hash(lettuce) == hash(eval( Meta.parse( string(flower(uidv, uid))))) 

Context: I discovered this problem using JSON3 where deserialisation/serialisation identities failed however could reproduce the issue in without those packages loaded. The same holds for uuid1. This is extremely cursed.

Some more testing yielded that:

cornflakes = string(flower(uidv, uid))
@assert cornflakes == string(flower(uidv, uid)) ## passes
soap = Meta.parse(cornflakes)
@assert soap == Meta.parse(cornflakes) ## passes

@assert eval(soap) == eval(soap) ## Fails

## this also fails
@assert eval(:(flower(UUID[UUID("c473d632-4815-11ee-047b-53ba6f201cba"), UUID("c473d644-4815-11ee-2bf5-01764c9f2b45"), UUID("c473d658-4815-11ee-361c-3d983a147777")], UUID("c4745718-4815-11ee-1ec9-fb22fba57306")))) == eval(:(flower(UUID[UUID("c473d632-4815-11ee-047b-53ba6f201cba"), UUID("c473d644-4815-11ee-2bf5-01764c9f2b45"), UUID("c473d658-4815-11ee-361c-3d983a147777")], UUID("c4745718-4815-11ee-1ec9-fb22fba57306"))))

## while those two pass
@assert eval(:(UUID[UUID("c473d632-4815-11ee-047b-53ba6f201cba"), UUID("c473d644-4815-11ee-2bf5-01764c9f2b45"), UUID("c473d658-4815-11ee-361c-3d983a147777")])) == eval(:(UUID[UUID("c473d632-4815-11ee-047b-53ba6f201cba"), UUID("c473d644-4815-11ee-2bf5-01764c9f2b45"), UUID("c473d658-4815-11ee-361c-3d983a147777")]))

@assert eval(:(UUID("c473d658-4815-11ee-361c-3d983a147777"))) == eval(:(UUID("c473d658-4815-11ee-361c-3d983a147777")))
vtjnash commented 1 year ago

I don't see the issue. There are two different arrays (in the car field), and they are not ===, so they are distinctly different flower structs with different == and hash. That seems to be what you are seeing too, as I would expect

jakobnissen commented 1 year ago

@freemin7 To expand on this a little, your issue is equivalent to this:

julia> struct Foo
           x::Vector{Int}
       end

julia> Foo([1]) == Foo([1])
false

This happens because == is not explicitly defined for Foo, so it falls back to the following fallback definition

==(x, y) = x === y

And since the two inner arrays are not === to each other, the result is false.

The === fallback is unfortunate. Issues https://github.com/JuliaLang/julia/issues/4648 and #40717 propose alternative (and IMO, better) solutions, but changing the default fallback is breaking, so it will probably never happen.

freemin7 commented 1 year ago

Well it's not what i expected. I thought i tested that case with.

@assert lettuce.car == eval( Meta.parse( string(flower(uidv, uid)))).car ## passes

It is extremely surprising behavior that

@assert ( Foo([1]).x == Foo([1]).x ) && not(Foo([1]) == Foo([1]))

And the documentation while truthful is only truthful by omission. When i work with UUIDs and serialization i think "someone might have messed up the interning" https://en.wikipedia.org/wiki/Interning_(computer_science) of UUIDs and while i read "Falls back to ===." in ?== that wasn't strong enough language to consider that it a sensible default wasn't chosen for structs. The "For example," makes the listing of exception to the fallback incomplete. "structs being a direct sub-type of Any use the fallback by default." would have been sufficiently explicit to grab my attention.

Another alternative would be a referencing a struct_equal https://github.com/JuliaLang/julia/issues/4648#issuecomment-1093896734 in a see also section if something like that existed in stdlib. Does something like that exist in Base already?

Either way the issue could be closed by a more explicit documentation.