- About Scala
- Documentation
- Code Examples
- Software
- Scala Developers
when is a zero not a zero?
Thu, 2009-06-11, 14:41
A nice side effect of working on the equals problem has been flushing
out other bugs. Here's a bit of a corker -- it turns out the algorithm
for == generation (which attempts to work around the fact that boxed
primitives of different types come back unequal even if they hold the
same value) was already busto. Any version of scala:
scala> def boop(x1: Number, x2: Number) = x1 == x2
boop: (x1: java.lang.Number,x2: java.lang.Number)Boolean
scala> boop(new java.lang.Long(0), new java.lang.Integer(0))
res0: Boolean = false
It happens because the code generator assumes that if the lhs and rhs
are both subtypes of Number and are of the same static type, then it can
use their regular equals method. Not so, as they can both be statically
Number but in reality different subtypes of Number. That logic is only
sufficient for final classes.
(I'll check in the fix with my String/RichString fix.)
Speaking of, I went with the canEqual method suggested by the good
Doctor MacIver and with only a relatively small bit of special-casing
for String, now Strings == RichStrings and vice versa, and neither is
ever equal to anything else. There are two chunks of special casing
required to make this work, neither of which is too offensive.
* compiler: if the LHS might be a String and the RHS might not,
the logic is basically "a.equals(b) || b.equals(a)" (but optimized
depending on how much we know.)
* library: all equals methods in Sequence-derived classes must
consult with canEqual before invoking their regular equals logic,
which always says false on RichStrings. What gives me pause about
that is that if anyone overrides equals in a collection they must
either duplicate the canEqual check or confirm with super, else
RichStrings will suddenly start seeming equal to certain seqs.
For instance, case object Nil overrides equals like this:
override def equals(that: Any) = that match {
case that1: Sequence[_] => that1.isEmpty
case _ => false
}
So until I went hunting for all the equals methods in collections,
(Nil == "".reverse) == true
as it does in trunk. Parenthetically, can we confirm that is the right
Nil logic? Should it not consult with super? It also seems rather broad.
This takes us to some unfortunate blatantly asymmetric equals methods in
the standard lib, like this one in scala.xml.PCData:
final override def equals(x: Any) = x match {
case s: String => s.equals(data)
case s: Atom[_] => data == s.data
case _ => false
}
and I don't even know what to say about this one in scala.xml.Group:
/** structural equality */
override def equals(x: Any) = x match {
case z:Group => (length == z.length) && sameElements(z)
case z:Node => (length == 1) && z == apply(0)
case z:Seq[_] => sameElements(z)
case z:String => text == z
case _ => false
}
PROPOSAL: String is granted a primitive-like status, and as already
takes place with boxed primitives, String == Foo always calls Foo's
equals method as well. This leaves us with "String specific"
special-casing at the compiler level rather than "RichString specific",
which to me is much more defensible. And as a bonus it fixes equals
methods such as the one in PCData, though I expect the one in Group
can't be fixed by anything short of catastrophic data loss.
More fun with numeric equality:
BigInt("1602246838364844302") ==
(new java.math.BigInteger("38495734985783947534"))
res0: Boolean = true
Note to selves: longValue() is not necessarily lossless...