Passing method arguments: Tuple vs Dict

What am I missing?

"""
A struct to hold parameters
""" 
struct P
    a :: Float64
    b :: Vector{Float64}
end

""" 
A method to update the parameters
""" 
function P(; a = 0.0, b = [0.0, 0.0])
    P(a, b)
end 

Create an instance of P by passing the parameters directly

julia> p0 = P(a = 1.0, b = [2.0, 3.0])
P(1.0, [2.0, 3.0])

Create an instance of P by passing the parameters via a dictionary

julia> d1 = Dict(:a => 1.0, :b => [2.0, 3.0])
julia> p1 = P(; d1...)
P(1.0, [2.0, 3.0])

Isn’t that the same thing? Ahem, no:

julia> p1 == p0
false  

And yet they have the same type:

julia> typeof(p0) == typeof(p1)
true 
julia> fieldnames(typeof(p0)) == fieldnames(typeof(p1))
true 

So what’s the difference? Thanks.

1 Like

By default, two structs are considered equal if they are bitwise the same. In your example, this is not the case because the two Vectors are different vectors (even though they have the same entries). The following example should make this clear:

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

julia> Foo([1,2]) == Foo([1,2]) # Different vectors with same entries
false

julia> a = [1,2]; Foo(a) == Foo(a)  # Same vector
true

If this is not the behaviour that you want, then you must define your own equality comparison:

julia> Base.:(==)(a::Foo,b::Foo) = a.a == b.a

julia> Foo([1,2]) == Foo([1,2]) 
true  # <- I guess this is what you wanted?
9 Likes

Thanks ettersi. I was just in the process of checking that I could pass the same data as a dictionary. I did not encounter this issue with scalars:

struct Foo
    a::Int64
end
julia> Foo(1) == Foo(1)
true

Why the difference between scalars and vectors? Thanks!

The key difference is mutable vs immutable data structures. Scalars are immutable: if you have two variables a = b = 1, then regardless what you do to a you will not be able to change the value of b. Vectors on the other hand are mutable: if you do a = b = [1] and a[1] = 2, then both a and b will now point to a vector [2]. Given this difference, it makes sense to make the default such that Foo(1) == Foo(1), but Foo([1]) != Foo([1]).

3 Likes

Makes perfect sense. Great answer, thanks ettersi!

1 Like

And don’t forget your custom Base.hash if you use it anywhere as a key (eg Dict).

1 Like

Thanks Tamas. Are you suggesting to overload Base.hash to assign a hash-key to each key,value pair inside the dictionary and Base.isequal to return true if all the hash-keys match? Am I understanding? Do you have a ready-made example of use?

I’ve just noticed this behaviour:

struct Foo
    a :: Real
end

julia> Foo(1) == Foo(1)
true

julia> Foo(1) == Foo(1.)
false

By contrast,

julia> 1 == 1.
true 

(but, as expected,

    julia> 1 === 1.
    false

).

The docs here and here are very clear about what == does and also the difference with isequal. Before reading the docs, I expected either false or a MethodError.

No, the point about hash is that if you define equality for your struct, then you should also define the way the struct itself is hashed so that isequal(x::Foo, y::Foo) implies hash(x) == hash(y), as specified here: Essentials · The Julia Language

In most cases this is extremely simple. Something like:

function Base.hash(x::Foo, h::UInt)
  hash(x.a, h)
end

would work just fine.

2 Likes

Thanks rdeits. I’m not familiar with any of this. :disappointed_relieved:

I want this to work for any number, so I type with Number, right?

struct Foo
   a::Vector{Number}
end

On the one hand:

julia> Base.isequal(x::Foo, y::Foo) = x.a == y.a
julia> Foo([1]) == Foo([1]) 
false 

On the other hand:

julia> Base.:(==)(x::Foo, y::Foo) = x.a == y.a
julia> Foo([1]) == Foo([1]) 
true

So there’s a subtle difference between == and isequal.

And what is this for?

julia> Base.hash(x::Foo, h::UInt) = hash(x.a, h)

julia> hash(Foo([1]))
0x8c5e337b3aca66f1

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

So while now Foo([1]) == Foo([1]) has become true, we have hash(Foo([1])) == hash(Foo([1])) is false. What do I do now?

What I gather from the docs is that if you create your own definition of equality between two things, you would also make sure that the two things are mapped to the same hash. As for what a hash is, I found this definition:

A hash is a function that converts one value to another. […] hashes are used to index data. Hashing values can be used to map data to individual “buckets” within a hash table. Each bucket has a unique ID that serves as a pointer to the original data. This creates an index that is significantly smaller than the original data, allowing the values to be searched and accessed more efficiently. (source)

The docs are short enough to be quoted in full:

Base.hash — Function

 hash(x[, h::UInt])

Compute an integer hash code such that `isequal(x,y)` implies `hash(x)==hash(y)`. The optional second argument `h` is a hash code to be mixed with the result.

New types should implement the 2-argument form, typically by calling the 2-argument `hash` method recursively in order to mix hashes of the contents with each other (and with `h`). Typically, any type that implements `hash` should also implement its own `==` (hence `isequal`) to guarantee the property mentioned above. Types supporting subtraction (operator `-`) should also implement [`widen`](@ref), which is required to hash values inside heterogeneous arrays.

I would recommend

struct Foo{T <: Number}
   a::Vector{T}
end

If you don’t understand the relative pros and cons of your proposal and mine, then I would recommend you open another topic since this issue is orthogonal to the main issue of this topic.


Have a look at the docstrings for == and isequal. The gist is that you should define only == unless you know what you are doing.


Hash functions are a very subtle topic, which is why I did not mention Base.hash initially. If you really want to know all the details about this, then I recommend you have a look at a couple of references online until you find one that works for you. If not, just go with what you proposed:

After defining this method, I get

julia> hash(Foo([1])) == hash(Foo([1]))
true

so I believe you obtaining

must be a consequence of some outdated method definitions. Try restarting your Julia REPL and repeating your experiment.

3 Likes

I have already accepted your answer from all the way up this thread. Thanks! I was initially only interested in understanding the behaviour of ==, with no plans to change its behaviour. I’m sure there are good reasons it behaves this way, and I have no plan to touch it. (If I’m hit by another unexpected false, I won’t care so much and move on)

Having said that, further down the discussion Tamas and Robin mentioned hash, which piqued my curiosity, as I’ve seen that come up in code once in a while (and the “don’t forget” made it sound possibly important). I thought perhaps this is the day I finally understand what it is for. But maybe not. :rofl:

I did have a look at the docs for == and isequal before posting, but the reason for the different behaviour did not jump out. The main difference stated in the doc is the treatment of floating point numbers and missing values, but there are none here, so I don’t know what’s going on. Here’s a quote from the doc:

Similar to == , except for the treatment of floating point numbers and of missing values.

As for the last point, you’re absolutely right, I messed up somewhere, it does work as expected: :flushed:

Try restarting your Julia REPL and repeating your experiment.

Thanks again. :+1:

1 Like