Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

Simulating IEqualityComparer using Implicits

41 views
Skip to first unread message

Travis Parks

unread,
Sep 21, 2016, 7:41:50 PM9/21/16
to scala-debate
I come from a pretty strong .NET background and I've been spoiled by the IEqualityComparer interface. For those not familiar, it allows you to provide an alternative GetHashCode and Equals implementation that a Dictionary (Map) or Set will use, rather than use the said methods on the objects in the container itself.

I've found this to be really useful in cases where a type has more than one way to uniquely identify it, such as a User type with a database primary key or a Windows AD domain name. I have an extension for .NET (https://github.com/jehugaleahsa/ComparerExtensions) that makes it possible to define IEqualityComparer objects given one or more lambdas.

Given that the Scala collections are based on Java collections, I don't suppose there's any chance that an IEqualityComparer interface will be introduced. I don't think this is really necessary, anyway. I've been wondering if you could define a wrapper class, given a lambda, that would provide a custom hashCode and equals implementation. Something like this:

class Mappable[T](val instance: T, private val accessor: T => Any) {
   
override def equals(other: Any): Boolean = other match {
       
case other : Mappable[T] => accessor(instance) equals accessor(other.instance)
       
case _ => false
   
}
   
override def hashCode(): Int = accessor(instance).hashCode
}

You'd define a Set[Mappable[User]], for example, and use implicit conversions to go back and forth to the underlying User object. Still, it'd be nice is methods like distinct, toSet and toMap would provide overrides accepting a lambda that did these conversions under the hood. Again, an implicit class could provide these overloads.

In cases where more than one field uniquely identifies a class, one could simply pass a tuple of the values, since tuples implement hashCode and equals anyway (e.g. u => (u.domain, u.userName)).

Honestly, most of the time I can get away with a Map[Int, X] or a Map[String, X]. However, there are times when I need to map a User to another object. Using an Int or String instead of an actual User means doing a second lookup to grab the user. For example:

val userLookup = Map[Int, User]() // Int is the userId
val roleLookup
= Map[Int, Role]() // Int is the userId
// ... populate the lookups
for ((userId, role) <- roleLookup) {
    val user
= userLookup(userId)
   
// ... do something with user and role
}

I was curious if anyone else made progress in this area. Not sure if other people ran into a similar situation.

Travis Parks

unread,
Sep 21, 2016, 10:19:21 PM9/21/16
to scala-debate
I went ahead and created some example code that uses implicits to automatically convert back and forth from an object to a Mappable[T]. Having to declare a Set[Mappable[T]] isn't the most elegant looking code, but it seems to do the trick.

class User {
   
var userId = 0
   
var userName = ""
}

class MappableAccessor[T](val accessor: T => Any) {
   
def apply(instance: T): Any = accessor(instance)
}

class Mappable[T](val instance: T)(private val accessor: MappableAccessor[T]) {

   
override def equals(other: Any): Boolean = other match {
       
case other : Mappable[T] => accessor(instance) equals accessor(other.instance)
       
case _ => false
   
}
   
override def hashCode(): Int = accessor(instance).hashCode
}

object Implicits {
   
import scala.language.implicitConversions

   
implicit def wrap[T](instance: T)(implicit accessor: MappableAccessor[T]): Mappable[T] =
       
new Mappable(instance)(accessor)
   
implicit def unwrap[T](wrapped: Mappable[T]): T = wrapped.instance
}

object Main extends App {
   
import Implicits._
   
implicit val accessor = new MappableAccessor[User](u => u.userName)
    val userSet
= scala.collection.mutable.Set[Mappable[User]]()
   
// Implicit conversions from User to Mappable[User]
    userSet
+= new User() { userId = 1; userName = "bob" }
    userSet
+= new User() { userId = 2; userName = "sally" }
    userSet
+= new User() { userId = 3; userName = "george" }
   
    userSet
+= new User() { userId = 2; userName = "sally" }  // add duplicate

   
Console.out.println(userSet.size)  // size is still 3

    val head
: User = userSet.head  // implicit conversion from Mappable[T] to T
   
Console.out.println(head.userName)
}


ARKBAN

unread,
Sep 22, 2016, 10:08:45 AM9/22/16
to scala-...@googlegroups.com

I think (and I could be wrong) that the typical practice in Scala is to use a case class as the Map key when you need a full blown object because of its implied immutable. A non-idempotent implementation of IEqualityComparer or the underlying object is a concern in situations like this, and case classes generally quiet those concerns (even if they can't eliminate it entirely).

ARKBAN
--
You received this message because you are subscribed to the Google Groups "scala-debate" group.
To unsubscribe from this group and stop receiving emails from it, send an email to scala-debate...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages