-
-
Save bvenners/8802510 to your computer and use it in GitHub Desktop.
One way Scalaz's === operator differs from ScalaUtils is that Scalaz does not define default | |
Equal instances whereas ScalaUtils does provide default Equality and Equivalence instances. | |
Scalaz provides Equal instances for Int and List[Int], for example, but it doesn't do so for | |
mutable objects. Thus by default you can compare Ints and List[Int]s with === in Scalaz, but | |
not arrays: | |
scala> import scalaz._ | |
import scalaz._ | |
scala> import Scalaz._ | |
import Scalaz._ | |
scala> 1 === 1 | |
res5: Boolean = true | |
scala> List(1, 2, 3) === List(1, 2, 3) | |
res6: Boolean = true | |
scala> Array(1, 2, 3) === Array(1, 2, 3) | |
<console>:14: error: could not find implicit value for parameter F0: scalaz.Equal[Array[Int]] | |
Array(1, 2, 3) === Array(1, 2, 3) | |
^ | |
By constrast, ScalaUtils allows arrays to be compared with ===, and moreover provides a | |
deep comparison: | |
scala> import org.scalautils._ | |
import org.scalautils._ | |
scala> import TypeCheckedTripleEquals._ | |
import TypeCheckedTripleEquals._ | |
scala> 1 === 1 | |
res0: Boolean = true | |
scala> List(1, 2, 3) === List(1, 2, 3) | |
res1: Boolean = true | |
scala> Array(1, 2, 3) === Array(1, 2, 3) | |
res2: Boolean = true | |
So one difference is that ScalaUtils === requires a lot less boilerplate code. Scalaz requires | |
you to define an Equal instance for every type you define that you want to compare for equality | |
with ===, ScalaUtils does not. Unless you want to provide a different equality from the one | |
provided by equals method on the object, you need not do anything special with ScalaUtils. | |
The reason Scalaz has this requirement, however, is because Scalaz aims to enable pure functional | |
programming and since arrays are mutable, that means never using them. Thus if you accidentally | |
use a mutable object and get so far as to compare it for equality with ===, the compiler will | |
notify you that of your transgression into the world of mutable objects. | |
In the tradition of ScalaTest, ScalaUtils is very flexible and customizable, so you can actually | |
achieve the same thing with ScalaUtils if you want that. One reason you might want to do so with | |
ScalaUtils is that Scalaz's === has some undesireable behavior. For example, even though one of | |
the equality laws is symmetry, the Scalaz === operator doesn't compile symmetrically. Here's | |
an example: | |
scala> 1 === 1L | |
<console>:14: error: could not find implicit value for parameter F0: scalaz.Equal[Any] | |
1 === 1L | |
^ | |
scala> 1L === 1 | |
res1: Boolean = true | |
In this case, it fails to compile a Long compared to an Int, unless you switch the order, then | |
it's fine. | |
A worse one pointed out by Eric Torreborre recently is this one: | |
scala> 1 === () | |
<console>:14: error: not enough arguments for method ===: (other: Int)Boolean. | |
Unspecified value parameter other. | |
1 === () | |
^ | |
scala> () === 1 | |
<console>:14: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses | |
() === 1 | |
^ | |
res3: Boolean = true | |
In the first case it fails to compile as desired, but only because the compiler interprets the empty | |
parentheses as something other than the Unit value. But in the second case, it happily converts the 1 | |
to a Unit value, then allows the comparison and returns true! | |
Here's how you'd use ScalaUtils to get a more sane === operator that will only work if a Scalaz Equal | |
instance is available: | |
import org.scalautils._ | |
import TripleEqualsSupport._ | |
import scalaz.Equal | |
trait LowPriorityScalazConstraint extends TypeCheckedTripleEquals { | |
import scala.language.implicitConversions | |
// Turn off the implicit Constraint providers of supertrait TypeCheckedTripleEquals that require an Equaivalence | |
override def lowPriorityTypeCheckedConstraint[A, B](implicit equivalenceOfB: Equivalence[B], ev: A <:< B): Constraint[A, B] = new AToBEquivalenceConstraint[A, B](equivalenceOfB, ev) | |
override def convertEquivalenceToAToBConstraint[A, B](equivalenceOfB: Equivalence[B])(implicit ev: A <:< B): Constraint[A, B] = new AToBEquivalenceConstraint[A, B](equivalenceOfB, ev) | |
override def typeCheckedConstraint[A, B](implicit equivalenceOfA: Equivalence[A], ev: B <:< A): Constraint[A, B] = new BToAEquivalenceConstraint[A, B](equivalenceOfA, ev) | |
override def convertEquivalenceToBToAConstraint[A, B](equivalenceOfA: Equivalence[A])(implicit ev: B <:< A): Constraint[A, B] = new BToAEquivalenceConstraint[A, B](equivalenceOfA, ev) | |
final class AToBEqualConstraint[A, B](equalOfB: Equal[B], cnv: A => B) extends Constraint[A, B] { | |
override def areEqual(a: A, b: B): Boolean = equalOfB.equal(cnv(a), b) | |
} | |
// Low priority implicit Constraint provides that require a Scalaz Equal | |
implicit def lowPriorityScalazConstraint[A, B](implicit equalOfB: Equal[B], ev: A <:< B): Constraint[A, B] = new AToBEqualConstraint[A, B](equalOfB, ev) | |
implicit def convertEqualToAToBConstraint[A, B](equalOfB: Equal[B])(implicit ev: A <:< B): Constraint[A, B] = new AToBEqualConstraint[A, B](equalOfB, ev) | |
} | |
trait ScalazTripleEquals extends LowPriorityScalazConstraint { | |
import scala.language.implicitConversions | |
final class BToAEqualConstraint[A, B](equalOfA: Equal[A], cnv: B => A) extends Constraint[A, B] { | |
override def areEqual(a: A, b: B): Boolean = equalOfA.equal(a, cnv(b)) } | |
// Implicit Constraint providers that require a Scalaz Equal | |
implicit def scalazCheckedConstraint[A, B](implicit equalOfA: Equal[A], ev: B <:< A): Constraint[A, B] = new BToAEqualConstraint[A, B](equalOfA, ev) | |
implicit def convertEqualToBToAConstraint[A, B](equalOfA: Equal[A])(implicit ev: B <:< A): Constraint[A, B] = new BToAEqualConstraint[A, B](equalOfA, ev) | |
} | |
object ScalazTripleEquals extends ScalazTripleEquals | |
Given this bit of code, all the problems with Scalaz's === mentioned above are fixed. You can | |
now do this: | |
scala> import scalaz.std.AllInstances._ | |
import scalaz.std.AllInstances._ | |
scala> import ScalazTripleEquals._ | |
import ScalazTripleEquals._ | |
scala> 1 === 1 | |
res0: Boolean = true | |
scala> List(1, 2, 3) === List(1, 2, 3) | |
res1: Boolean = true | |
scala> Array(1, 2, 3) === Array(1, 2, 3) | |
<console>:14: error: types Array[Int] and Array[Int] do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalautils.Constraint[Array[Int],Array[Int]] | |
Array(1, 2, 3) === Array(1, 2, 3) | |
^ | |
scala> () === 1 | |
<console>:14: error: types Unit and Int do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalautils.Constraint[Unit,Int] | |
() === 1 | |
^ | |
scala> 1 === () | |
<console>:14: error: types Int and Unit do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalautils.Constraint[Int,Unit] | |
1 === () | |
^ | |
scala> 1 === 1L | |
<console>:14: error: types Int and Long do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalautils.Constraint[Int,Long] | |
1 === 1L | |
^ | |
scala> 1L === 1 | |
<console>:14: error: types Long and Int do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalautils.Constraint[Long,Int] | |
1L === 1 | |
^ |
Thanks for the clarification. I think that in practice people expect mutable objects to be stable at the time they compare them for equality. People want to do this kind of thing with mutable objects in tests all the time:
scala> import org.scalatest._
import org.scalatest._
scala> import Assertions._
import Assertions._
scala> val result = Array(1, 2) :+ 3
result: Array[Int] = Array(1, 2, 3)
scala> assert(result === Array(1, 2, 3))
And even:
scala> result(2) = 7 // blasphemy!
scala> assert(result === Array(1, 2, 7))
At the time you look, the value is effectively (at least long enough for the comparison) immutable.
Anyway, I like the purity of the ideal. I wanted to show that this is possible with ScalaUtils === as well with a few lines of code. It is in fact something I could add to ScalaUtils, but I wouldn't want to add another choice without a solid use case such that users would really benefit from it.
The problem with any "in practice, people expect…" notions are that in practice, reality diverges from expectations.
Specifically, these expectations assume serial execution, and fail in the face of parallelism and concurrency.
While it may well be convenient for a specific use such as a serial test, this convenience does come at a (hidden) complexity cost.
Nevertheless, there are a couple of nice features shown here!
That's not really the reason. The reason equality isn't defined for
Array
is because arrays are not values. Or rather, the component ofArray
that we think of as being the container for values is not a value. The only "value-like" part of an array is the pointer identity. Thus, if we definedEqual
forArray[A: Equal]
, we would find ourselves with an equality that isn't necessarily stable. Two immutable values,x
andy
, could be equal one moment and not-equal the next. This isn't just impure: it flies completely in the face of any sane notion of "value".The only real way that you can define a stable equality on
Array
is to define it according to its pointer identity, which is precisely what Java does by default (Object#equals
). However, this exposes an implementation detail and provides properties so limited that such a definition would be essentially useless anyway.Scalaz's asymmetry with respect to the numeric types is definitely super-annoying. I wish they would fix that.