Dict equality with composite type value

Still new to Julia and trying to test one of my methods which return a Dict. I know what should be included in this Dict and I try to compare it with what is returned by my method. However I have difficulties to test equality between two Dicts.

Here is a minimal working example of my problem :

Module.jl

module Module
import Base

export Struct
export Simple

struct Struct
        id::Int
        x::Vector{Float64}
end

struct Simple
        id::Int
end
end

runtests.jl

import Base
using Module
using Test

@testset "simple" begin
        expected = Dict{Int, Module.Simple}()
        expected[1] = Module.Simple(1)
        actual = Dict{Int, Module.Simple}()
        actual[1] = Module.Simple(1)
        @test actual == expected
end

@testset "struct" begin
        expected = Dict{Int, Module.Struct}()
        expected[1] = Module.Struct(1, [1.0])
        actual = Dict{Int, Module.Struct}()
        actual[1] = Module.Struct(1, [1.0])
        @test actual == expected
end

For the Simple struct, test passed without any problem.
For the Struct struct, test failed with the following error :

(Module) pkg> test
   Testing Module
 Resolving package versions...
Test Summary: | Pass  Total
simple        |    1      1
struct: Test Failed at /home/xxxxxxx/sandbox/Module/test/runtests.jl:18
  Expression: actual == expected
   Evaluated: Dict(1 => Struct(1, [1.0])) == Dict(1 => Struct(1, [1.0]))
Stacktrace:
 [1] top-level scope at /home/xxxxxxxxx/sandbox/Module/test/runtests.jl:18
 [2] top-level scope at /build/julia/src/julia-1.2.0/usr/share/julia/stdlib/v1.2/Test/src/Test.jl:1113
 [3] top-level scope at /home/xxxxxxxx/sandbox/Module/test/runtests.jl:14
Test Summary: | Fail  Total
struct        |    1      1

First, I have strictly no idea why this error occurs. Is it related to the fact that Simple is strictly immutable and Struct is not (Vector is an array and arrays are mutable in julia if i get it right) ?

I try to find an answer in julia documentation and discourse and I thought :

  • I should create following methods : Base.isequal(struct::Struct) and Base.hash(struct::Struct)
  • I should replace Struct variables by strictly immutable types

So I have the following questions :

  • Why this error occurs ?
  • Should I create a specific Base.isequal/hash for each new struct I create ?
  • Is there any way to rely on default Dict implementation to test equality between two dicts with custom composite types ?

Thank you!

What you are running into is that == for custom immutable structs just ends up falling back to ===, and because your two arrays you pass to Struct don’t have the same location in memory, this returns false. There is already an open issue for this here. The solution in your case would be to overload Base.:(==) or Base.isequal for Struct. You might not even need to overload hash, but I would test that again first.

1 Like

Thanks for your answer and the link to the issue.
I understand now why for Struct it does not work and throw an error.
Just have a follow up question for the Simple case :

@testset "identity/equals" begin
        a = 1
        b = 1
        @test 1 === 1 # pass => OK
        @test [1.0] === [1.0] # fail => OK
        @test [1.0] == [1.0] # pass => OK
        @test  a === b # pass => Why ?
end

In this test a and b should not have the same location in memory and the identity comparison still return true. Is there a special case for primitive types ?

There is no special case for primitive types, in julia, the variables use the memory used to store its value, each variable does not have its own memory. So a and b have the same location, and it is normal that pass the identity comparison. When you say var1 = var2, var1 and var2 are alias, each variable has not its own memory, the memory is the same, the memory needed to store the value.

b = [1.0]
a = b
@test a === b  # true

Actually, when the variable value is scalar, its behavior is similar to have its own memory.

a = 4
b = a # Both are the same value and memory position
a === b # true
a = 5 # Now a make reference to constant 5, stored in a different  memory position
a === b # false
println(b) # Print 4
println(a) # Print 5 
2 Likes

By the way, as @simeonschaub said, your code will pass the tests doing:

import Base: ==

function ==(val1::Module.Struct, val2::Module.Struct)
    return val1.id == val2.id && val1.x == val2.x
end
1 Like

Thank you for your answer.
I need more insight :

I quote :

So a and b have the same location, and it is normal that pass the identity comparison

a = 1
b = 1
a === b # true

why in this case a and b refers to the same memory location ?

Thank you !

It doesn’t. Forget about memory locations, they are very misleading in understanding ===.

The best definition is in its docstring:

Determine whether x and y are identical, in the sense that no program could distinguish them.

Also, if you end up defining an == method for something, it is usually advantageous to define Base.hash too right away. It is very easy to end up with your values in a Dict or something similar, and then chase a silly bug for hours just because hash does not match ==.

2 Likes

Thank you for your answer.

Ok I had to focus more on === documentation.
Indeed integer literals 1 could not be distinguished.
Then, how julia distinguish arrays (first guess : allocated on the heap at different location? )

a = [1.0] 
b = [1.0]
a === b # false

I agree with you both == and hash should be overloaded right away.

One key ingredient about the efficiency is Julia is that the “how” is left unspecified, so it leaves considerable room for the compiler to do clever things. So unless you want to understand internals (in which case dig into the source and follow the devdocs), don’t be concerned about this.

What is clear that one could devise a conforming program could distinguish them (eg change some values and check if they change in the other one), so they are not equal. Whereas eg

using StaticArrays
a = SVector(1, 2)
b = SVector(1, 2)
a === b # true
2 Likes

Thanks ! All clear now !