Skip to content

Instantly share code, notes, and snippets.

@bvenners
Last active August 29, 2015 13:56
Show Gist options
  • Save bvenners/8802510 to your computer and use it in GitHub Desktop.
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
^
@jedws
Copy link

jedws commented Feb 4, 2014

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment