# Edge case in Sets with Floats

**URL:** <https://discourse.julialang.org/t/edge-case-in-sets-with-floats/78787>\
**Category:** General Usage\
**Tags:** collection, set, isequal, hash, sets\
**Created:** [March 31, 2022, 8:46am UTC](https://discourse.julialang.org/t/edge-case-in-sets-with-floats/78787 "2022-03-31T08:46:11Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![fatteneder](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/fatteneder/32/33991_2.png) [@fatteneder](https://discourse.julialang.org/u/fatteneder)\
**Post date:** [March 31, 2022, 8:46am UTC](https://discourse.julialang.org/t/edge-case-in-sets-with-floats/78787/1 "2022-03-31T08:46:11Z")

</div>

Hi Julia Community,

recently I was approached by a colleague who just got started with Julia and ran into a problem that took him ages to debug. We then seat down together and could come up with the following MWE:

```julia
julia> 0 in Set(-0)
True
julia> 0.0 in Set(-0.0)
False

```

Although, unexpected from a mathematical point of view, the result is plausible if you look into the implementation: The elements of a `Set{T}` are stored as the keys of a `Dict{T,Nothing}`. Using `in` then boils down to `hash`ing the keys and comparing those. And since `bitstring(0) == bitstring(-0)` and `bitstring(0.0) != bitstring(-0.0)` we get the above result.

Can this be considered bug? Or is it assumed that users of `Set` should know that comparisons are done using `hash` instead of `isequal`?  
Unfortunately, the docstring of `Set` does not say anything about his behavior: [Collections and Data Structures · The Julia Language](https://docs.julialang.org/en/v1.7/base/collections/#Base.Set)

Workaround (not recommended, because of type piracy): Collect the keys and use `in` for `Vectors` which falls back to `==`:

```julia
Base.in(x, s::Set{T}) where T<:Real = x in collect(keys(s.dict))

```

But I am not sure whether this can be added without breaking anything else within Julia.

Tested with `versioninfo()`:

```julia
julia> versioninfo()
julia Version 1.7.2
Commit bf53498635 (2022-02-06 15:21 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin19.5.0)
  CPU: Intel(R) Core(TM) i5-4250U CPU @ 1.30GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-12.0.1 (ORCJIT, haswell)

```

Cheers

---

<div class="post-metadata">

**Author:** ![lawless-m](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lawless-m/32/30869_2.png) [@lawless-m](https://discourse.julialang.org/u/lawless-m)\
**Post date:** [March 31, 2022, 8:58am UTC](https://discourse.julialang.org/t/edge-case-in-sets-with-floats/78787/2 "2022-03-31T08:58:38Z")

</div>

```julia
julia> isequal(0.0, -0.0)
false

```

[https://docs.julialang.org/en/v1/manual/integers-and-floating-point-numbers/#Floating-point-zero](https://docs.julialang.org/en/v1/manual/integers-and-floating-point-numbers/#Floating-point-zero)

The documentation for `in` however, does document this behaviour

[https://docs.julialang.org/en/v1.7/base/collections/#Base.in](https://docs.julialang.org/en/v1.7/base/collections/#Base.in)

> For example, [`Set`](https://docs.julialang.org/en/v1.7/base/collections/#Base.Set)s check whether the item [`isequal`](https://docs.julialang.org/en/v1.7/base/base/#Base.isequal) to one of the elements.

---

<div class="post-metadata">

**Author:** ![nilshg](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nilshg/32/2283_2.png) [@nilshg](https://discourse.julialang.org/u/nilshg)\
**Post date:** [March 31, 2022, 9:02am UTC](https://discourse.julialang.org/t/edge-case-in-sets-with-floats/78787/3 "2022-03-31T09:02:50Z")

</div>

And I guess

> **[Signed zero](https://en.wikipedia.org/wiki/Signed_zero)**
>
> Signed zero is zero with an associated sign. In ordinary arithmetic, the number 0 does not have a sign, so that −0, +0 and 0 are equivalent. However, in computing, some number representations allow for the existence of two zeros, often denoted by −0 (negative zero) and +0 (positive zero), regarded as equal by the numerical comparison operations but with possible different behaviors in particular operations. This occurs in the sign-magnitude and ones' complement signed number representations for i...

in particular

> The [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) standard for floating-point arithmetic (presently used by most computers and programming languages that support floating-point numbers) requires both +0 and −0.

---

<div class="post-metadata">

**Author:** ![fatteneder](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/fatteneder/32/33991_2.png) [@fatteneder](https://discourse.julialang.org/u/fatteneder)\
**Post date:** [March 31, 2022, 9:37am UTC](https://discourse.julialang.org/t/edge-case-in-sets-with-floats/78787/4 "2022-03-31T09:37:30Z")

</div>

With the above links I should rephrase my question:

> Can this be considered bug? Or is it assumed that users of `Set` should know that comparisons are done using `hash` ~~instead of `isequal`~~ and `isequal` (applied to the hashes)?

The first one can be answered by no and the second is answered by lawless-m’s reply.

Regarding the ‘workaround’:

```julia
Base.in(x, s::Set{T}) where T<:Real = x in collect(keys(s.dict))

```

This does not fall back to `isapprox`, but instead to `==` which can be checked with `@edit 0.0 in [-0.0]`.  
And `==` is not the same as `isequal` in the case of floating point numbers, which I wasn’t aware of. See also ([Essentials · The Julia Language](https://docs.julialang.org/en/v1.7/base/base/#Base.isequal))

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

Thank you for your answers!

---

<div class="post-metadata">

**Author:** ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)\
**Post date:** [March 31, 2022, 9:56am UTC](https://discourse.julialang.org/t/edge-case-in-sets-with-floats/78787/5 "2022-03-31T09:56:31Z")

</div>

> [@fatteneder](#):
>
> Regarding the ‘workaround’:
> 
> ```julia
> Base.in(x, s::Set{T}) where T<:Real = x in collect(keys(s.dict))
> 
> ```

Note that you’re defining a method for a function you didn’t define, for types you don’t own (i.e. not your own types). This can (will) lead to unexpected behavior for users of your code, since that has non-local effects on how the code of others behaves when your code is loaded as well. This is called “type piracy” and (if required to solve a problem) is generally a bad idea for the reasons mentioned above.

---

<div class="post-metadata">

**Author:** ![fatteneder](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/fatteneder/32/33991_2.png) [@fatteneder](https://discourse.julialang.org/u/fatteneder)\
**Post date:** [March 31, 2022, 11:18am UTC](https://discourse.julialang.org/t/edge-case-in-sets-with-floats/78787/6 "2022-03-31T11:18:36Z")

</div>

Thanks for explicitly pointing that out and (since I am already aware of type piracy) I second this. I ll update my initial post to make it clear.

---

<div class="post-metadata">

**Author:** ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)\
**Post date:** [March 31, 2022, 12:25pm UTC](https://discourse.julialang.org/t/edge-case-in-sets-with-floats/78787/7 "2022-03-31T12:25:00Z")

</div>

> [@fatteneder](#):
>
> Collect the keys and use `in` for `Vectors` which falls back to `==` :

Besides the fact that this is type piracy, the performance of this will be truly awful.

You could define a wrapper type for your keys that impelments a custom `hash` and `isequal` which ignore the sign of zero. This also avoids type piracy. For example:

```julia
struct FloatKey{T<:AbstractFloat}
    val::T
end
Base.isequal(x::FloatKey, y::FloatKey) = iszero(x.val) ? x.val == y.val : isequal(x.val, y.val)
Base.hash(x::FloatKey, h::UInt) = iszero(x.val) && signbit(x.val) ? hash(-x.val, h) : hash(x.val, h)
Base.ht_keyindex(d::Dict{FloatKey{T}}, x::Real) where {T, V} = Base.ht_keyindex(d, FloatKey(T(x)))

```

which gives

```julia
julia> s = Set(FloatKey.([-0.0, 0.0, 1.0, 2.0]))
Set{FloatKey{Float64}} with 3 elements:
  FloatKey{Float64}(0.0)
  FloatKey{Float64}(2.0)
  FloatKey{Float64}(1.0)

julia> 0.0 in s
true

julia> -0.0 in s
true

julia> 3.0 in s
false

julia> 1.0 in s
true

```
