== is not always transitive

177 views
Skip to first unread message

Patrick Houk

unread,
Sep 2, 2011, 3:50:45 PM9/2/11
to Clojure
Greetings,

I think that I've encountered a bug in ==.

user=> (and (== 1 1.0) (== 1.0 1.0M) (not (== 1 1.0M)))
true

This happens with 1.2.1 and 1.3-beta2. I think it has to do with the
precision of the BigDecimal.

user=> (== 1 1.0M)
false
user=> (== 1 1M)
true

I think a solution would be to use BigDecimal#compareTo (or maybe
BigDecimal#stripTrailingZeros) in ==, so that (== 1M 1.0M) becomes
true. (I would expect (= 1M 1.0M) to remain false, though.)

Thanks,
- Pat

Leif

unread,
Apr 11, 2012, 8:46:10 PM4/11/12
to clo...@googlegroups.com

I'd also like to make sure people are aware of this oddity.  I discovered this after reading an article about the bad design of PHP.  I read that in PHP, "== is not transitive."  I thought "Ha ha ha, that ridiculous PHP!"

Then I checked c.c/== ;  Imagine my reaction when I learned that Clojure had something in common with PHP.  o_O,  :'[
Other emoticons also washed over me.

In summary, beware when comparing different numeric types, and don't throw stones even if your house is made of bricks, because the windows are still glass.

Cedric Greevey

unread,
Apr 11, 2012, 10:00:52 PM4/11/12
to clo...@googlegroups.com
IME, it's almost never useful to perform equality tests on floating
point values. Generally you want to know if they're near enough to one
another without necessarily being exactly equal. For that something
like (defn f= [f1 f2 threshold] (< (Math/abs (- f1 f2)) threshold)) is
probably the sort of thing you want.

Sean Corfield

unread,
Apr 12, 2012, 12:05:24 AM4/12/12
to clo...@googlegroups.com
On Wed, Apr 11, 2012 at 5:46 PM, Leif <leif.p...@gmail.com> wrote:
> Then I checked c.c/== ;  Imagine my reaction when I learned that Clojure had
> something in common with PHP.  o_O,  :'[

It's instructive to look at the result of:

(let [ones [1 1.0 1N 1M 1.0M] ] (for [a ones b ones] (== a b)))

The first four compare equal in all combinations. 1.0M is equal to 1.0
but unequal to all the others.
--
Sean A Corfield -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
World Singles, LLC. -- http://worldsingles.com/

"Perfection is the enemy of the good."
-- Gustave Flaubert, French realist novelist (1821-1880)

Patrick Houk

unread,
Apr 12, 2012, 5:00:39 PM4/12/12
to Clojure
Yes, that is one reason why I tend to use BigDecimal instead of float
or double. The thing that seems wrong to me is (not (== 1 1.0M)),
since these are both exact representations of the value one and the
doc for == says that it tests for "equivalent value (type-
independent)".

Sung Pae

unread,
Apr 11, 2012, 11:44:05 PM4/11/12
to clo...@googlegroups.com
Leif <leif.p...@gmail.com> writes:

> I'd also like to make sure people are aware of this oddity. I
> discovered this after reading an article about the bad design of PHP.
> I read that in PHP, "== is not transitive." I thought "Ha ha ha, that
> ridiculous PHP!"
>
> Then I checked c.c/== ; Imagine my reaction when I learned that
> Clojure had something in common with PHP. o_O, :'[ Other emoticons
> also washed over me.

Clojure's == is numerical equivalence, not equality.

-------------------------
clojure.core/==
([x] [x y] [x y & more])
Returns non-nil if nums all have the equivalent
value (type-independent), otherwise false

The problem with PHP (and JavaScript's) == operator is that the type
coercion is not limited to numerical types:

PHP

"1" == 1 => true
"1" + 1 => 2

JavaScript

"1" == 1 => true
"1" + 1 => "11"

Clojure

(== "1" 1) => java.lang.ClassCastException
(+ "1" 1) => java.lang.ClassCastException

Clojure's equality function [1], however, does what you expect when
comparing floats and integers:

(= 1.0 1) => false

But it also does what you want when comparing _categories_ of numbers
[2] [3]:

(let [a (int 1) b (bigint 1)]
{:equal_values? (= a b)
:equal_types? (= (type a) (type b))})

=> {:equal_values? true, :equal_types? false}

I think Clojure does a fantastic job of being both convenient and
strongly typed.

--
guns

[1]: https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Util.java#L23
[2]: https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Numbers.java#L213
[3]: https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Numbers.java#L978

If you were really concerned about whether a number is a BigInteger
vs Integer, or Float vs Double, you would have to handle that on
your own.

Andrea Chiavazza

unread,
Apr 15, 2012, 2:22:10 PM4/15/12
to clo...@googlegroups.com
I must agree that the behaviour of == is not correct here.

The problem is in this method in Numbers.java:
public boolean equiv(Number x, Number y){
return toBigDecimal(x).equals(toBigDecimal(y));
}

The behaviour we currently have is:
user=> (let [ones [1 1.0 1N 1M 1.0M 1.00M] ] (doseq [a ones b ones] (println a "==" b \tab (== a b) )))
1 == 1 true
1 == 1.0   true
1 == 1N   true
1 == 1M   true
1 == 1.0M false
1 == 1.00M false
1.0 == 1   true
1.0 == 1.0 true
1.0 == 1N   true
1.0 == 1M true
1.0 == 1.0M true
1.0 == 1.00M true
1N == 1   true
1N == 1.0   true
1N == 1N   true
1N == 1M   true
1N == 1.0M false
1N == 1.00M false
1M == 1   true
1M == 1.0 true
1M == 1N   true
1M == 1M true
1M == 1.0M false
1M == 1.00M false
1.0M == 1 false
1.0M == 1.0 true
1.0M == 1N false
1.0M == 1M false
1.0M == 1.0M true
1.0M == 1.00M false
1.00M == 1 false
1.00M == 1.0 true
1.00M == 1N false
1.00M == 1M false
1.00M == 1.0M false
1.00M == 1.00M true

I propose we change the method to be:
public boolean equiv(Number x, Number y){
return toBigDecimal(x).compareTo(toBigDecimal(y)) == 0;
}
This makes the previous expression return all true, and == should also be transitive.

In particular, (== 1.0M 1.00M) will be true rather than false, which is much more in the spirit of ==.
The difference between 1.0M and 1.00M should be checked by calling the scale() method, which makes sense because it is quite an unusual thing to do.

I also noticed in test_clojure/numbers.clj the lines:
; TODO:
; ==
; and more...

So I thought of also adding the test:

(deftest equality
  (are [x y] (== x y)
    42    42
    42    42.0
    42    42N
    42    42M
    42    42.0M
    42    42.00M
    42.0  42
    42.0  42.0
    42.0  42N
    42.0  42M
    42.0  42.0M
    42.0  42.00M
    42N   42
    42N   42.0
    42N   42N
    42N   42M
    42N   42.0M
    42N   42.00M
    42M   42
    42M   42.0
    42M   42N
    42M   42M
    42M   42.0M
    42M   42.00M
    42.0M 42
    42.0M 42.0
    42.0M 42N
    42.0M 42M
    42.0M 42.0M
    42.0M 42.00M

    1.23  1.23
    1.23  1.23M
    1.23M 1.23
    1.23M 1.23M )

  (are [x y] (not (== x y))
    12 12.1
    1.23 123
    34 3.4
    1.23 1.234
    123N 345N
    123 345N
    123N 345
    12.34M 456N
    12.34M 4.56
    12.34 4.56M
    12 4.56M
    12M 4.56
    12.34M 1.234M ))

I think it would be really nice to get this fixed before the 1.4 release.

Can anybody defend the current behaviour against the one I propose ?
Reply all
Reply to author
Forward
0 new messages