score:4

Accepted answer

you can define a few internablen[arg1, arg2, ..., resulttype] traits for n being the number of arguments to apply(): internable1[a,z], internable2[a,b,z], etc. these traits define the cache itself, the intern() method and the apply method we want to hijack.

we'll have to define a trait (or an abstract class) to assure your internablen traits that there is indeed an apply method to be overriden, let's call it applyable.

trait applyable1[a, z] {
  def apply(a: a): z
}
trait internable1[a, z] extends applyable1[a, z] {
  private[this] val cache = weakhashmap[(a), z]()
  private[this] def intern(args: (a))(builder: => z) = {
    cache.getorelse(args, {
      val newobj = builder
      cache(args) = newobj
      newobj
    })
  }
  abstract override def apply(arg: a) = {
    println("internable1: hijacking apply")
    intern(arg) { super.apply(arg) }
  }
}

the companion object of your class will have to be a mixin of a concrete class implementing applyablen with internablen. it would not work to have apply directly defined in your companion object.

// class with one apply arg 
abstract class someclasscompanion extends applyable1[int, someclass] {
  def apply(value: int): someclass = {
    println("original apply")
    new someclass(value)
  }
}
class someclass(val value: int)
object someclass extends someclasscompanion with internable1[int, someclass]

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:

trait applyable2[a, b, z] {
  def apply(a: a, b: b): z
}
trait internable2[a, b, z] extends applyable2[a, b, z] {
  private[this] val cache = weakhashmap[(a, b), z]()
  private[this] def intern(args: (a, b))(builder: => z) = {
    cache.getorelse(args, {
      val newobj = builder
      cache(args) = newobj
      newobj
    })
  }
  abstract override def apply(a: a, b: b) = {
    println("internable2: hijacking apply")
    intern((a, b)) { super.apply(a, b) }
  }
}

// class with two apply arg 
abstract class anotherclasscompanion extends applyable2[string, string, anotherclass] {
  def apply(one: string, two: string): anotherclass = {
    println("original apply")
    new anotherclass(one, two)
  }
}
class anotherclass(val one: string, val two: string)
object anotherclass extends anotherclasscompanion with internable2[string, string, anotherclass]

the interaction shows that the internables' apply method executes prior to the original apply() which gets executed only if needed.

scala> import someclass._
import someclass._

scala> someclass(1)
internable1: hijacking apply
original apply
res0: someclass = someclass@2e239525

scala> import anotherclass._
import anotherclass._

scala> anotherclass("earthling", "greetings")
internable2: hijacking apply
original apply
res1: anotherclass = anotherclass@329b5c95

scala> anotherclass("earthling", "greetings")
internable2: hijacking apply
res2: anotherclass = anotherclass@329b5c95

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.

score:1

maybe a little hacky, but you could try defining your own intern() method, like java's string has:

import scala.collection.mutable.{map=>mutablemap}

object hashconsed {
  val cache: mutablemap[(class[_],int), hashconsed] = mutablemap.empty
}

trait hashconsed {
  def intern(): hashconsed = 
    hashconsed.cache.getorelse((getclass, hashcode), {
      hashconsed.cache((getclass, hashcode)) = this
      this
    })
}

case class foo(bar: int, baz: string) extends hashconsed

val foo1 = foo(1, "one").intern()
val foo2 = foo(1, "one").intern()

println(foo1 == foo2) // true
println(foo1 eq foo2) // true

Related Query

More Query from same tag