I’m looking for a way to have classes that behave just like case classes, but that are automatically hash consed.
One way to achieve this for integer lists would be:
import scala.collection.mutable.{Map=>MutableMap}
sealed abstract class List
class Cons(val head: Int, val tail: List) extends List
case object Nil extends List
object Cons {
val cache : MutableMap[(Int,List),Cons] = MutableMap.empty
def apply(head : Int, tail : List) = cache.getOrElse((head,tail), {
val newCons = new Cons(head, tail)
cache((head,tail)) = newCons
newCons
})
def unapply(lst : List) : Option[(Int,List)] = {
if (lst != null && lst.isInstanceOf[Cons]) {
val asCons = lst.asInstanceOf[Cons]
Some((asCons.head, asCons.tail))
} else None
}
}
And, for instance, while
scala> (5 :: 4 :: scala.Nil) eq (5 :: 4 :: scala.Nil)
resN: Boolean = false
we get
scala> Cons(5, Cons(4, Nil)) eq Cons(5, Cons(4, Nil))
resN: Boolean = true
Now what I’m looking for is a generic way to achieve this (or something very similar). Ideally, I don’t want to have to type much more than:
class Cons(val head : Int, val tail : List) extends List with HashConsed2[Int,List]
(or similar). Can someone come up with some type system voodoo to help me, or will I have to wait for the macro language to be available?
You can define a few
InternableN[Arg1, Arg2, ..., ResultType]traits for N being the number of arguments toapply():Internable1[A,Z],Internable2[A,B,Z], etc. These traits define the cache itself, theintern()method and theapplymethod we want to hijack.We’ll have to define a trait (or an abstract class) to assure your
InternableNtraits that there is indeed an apply method to be overriden, let’s call itApplyable.The companion object of your class will have to be a mixin of a concrete class implementing
ApplyableNwithInternableN. It would not work to have apply directly defined in your companion object.One good thing about this is that the original apply need not be modified to cater for interning. It only creates instances and is only called when they need to be created.
The whole thing can (and should) also be defined for classes with more than one argument. For the two-argument case:
The interaction shows that the Internables’ apply method executes prior to the original
apply()which gets executed only if needed.I chose to use a WeakHashMap so that the interning cache does not prevent garbage collection of interned instances once they’re no longer referenced elsewhere.
Code neatly available as a Github gist.