0__
0__

Reputation: 67290

Type-safe equals macro?

Is there a type-safe equals === implementation for Scala that has zero overhead over ==? That is, unlike === in Scalaz and ScalaUtils, an implementation that uses a straight macro to perform the check?

I would like to use === in many places but these are hot-spots, so I don't want that to incur any extra runtime costs (like constructing type classes and such).

Upvotes: 4

Views: 296

Answers (2)

0__
0__

Reputation: 67290

The answer based on Machinist is probably best. Here is a more hackish variant that detects cases such as inferring Any or AnyRef or the typical mix of two unrelated case classes (Product with Serializable):

import scala.collection.breakOut
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

object Implicits {
  implicit class TripleEquals[A](a: A) {
    def === [B >: A](b: B): Boolean = macro Macros.equalsImpl[A, B]
  }
}

object Macros {
  private val positiveList = Set("scala.Boolean", "scala.Int", "scala.Long",
                                 "scala.Float", "scala.Double", "scala.Option)
  private val negativeList = Set("java.lang.Object", "java.io.Serializable",
                                 "<refinement>")

  def equalsImpl[A: c.WeakTypeTag, B: c.WeakTypeTag](c: blackbox.Context)
                                                    (b: c.Expr[A]): c.Tree = {
    import c.universe._
    val bTpe = weakTypeOf[B]
    val base = bTpe.baseClasses
    val names: Set[String] = base.collect {
      case sym if sym.isClass => sym.fullName
    } (breakOut)

    // if a primitive is inferred, we're good. otherwise:
    if (names.intersect(positiveList).isEmpty) {
      // exclude all such as scala.Product, scala.Equals
      val withoutTopLevel = names.filterNot { n =>
        val i = n.lastIndexOf('.')
        i == 5 && n.startsWith("scala")
      }
      // exclude refinements and known Java types
      val excl = withoutTopLevel.diff(negativeList)
      if (excl.isEmpty) {
        c.abort(c.enclosingPosition, s"Inferred type is too generic: `$bTpe`")
      }
    }

    // now simply rewrite as `a == b`
    val q"$_($a)" = c.prefix.tree
    q"$a == $b"
  }
}

This doesn't work with higher-kinded types, yet, so tuples are deliberately failing, while unfortunately Some(1) === Some("hello") compiles.


Edit: A built a small library that improves on this to support higher-kinded types.

Upvotes: 0

Gabriele Petronella
Gabriele Petronella

Reputation: 108121

I think you can achieve it easily with machinist.

The README on GitHub gives exactly the === example:

import scala.{specialized => sp}

import machinist.DefaultOps

trait Eq[@sp A] {
  def eqv(lhs: A, rhs: A): Boolean
}

object Eq {
  implicit val intEq = new Eq[Int] {
    def eqv(lhs: Int, rhs: Int): Boolean = lhs == rhs
  }

  implicit class EqOps[A](x: A)(implicit ev: Eq[A]) {
    def ===(rhs: A): Boolean = macro DefaultOps.binop[A, Boolean]
  }
}

then you can use === with zero overhead (no extra allocations, no extra indirection) over ==


If you are looking for a out-of-the-box implementation, spire (from which machinist originated) provides one.

Also cats provides one.

They're both macro-based as they use machinist for the implementation.

Upvotes: 1

Related Questions