Silvio Bierman
Silvio Bierman

Reputation: 713

How to implement instance sharing for case classes

Assuming defintion:

case class IntegerWrapper(i : Int)

and being in a situation that potentially large amounts of IntegerWrapper instances with i=[0..N> may be created what does one have to do to:

  1. Map this range to a fixed set of singletons [IntegerWrapper(0) .. IntegerWrapper(N)>

  2. Preserve existing value semantics for class IntegerWrapper (matching, equals, hashcode, serialization)

I am looking to do instance sharing akin to what java.lang.Integer does. I guess my question is if this can be done without having to do everything myself. Simply defining a companion object with an apply(i : Int) does not compile. Any suggestions?

Upvotes: 5

Views: 478

Answers (4)

Malte Schwerhoff
Malte Schwerhoff

Reputation: 12852

Something like this?

sealed trait IntegerWrapper {
  def i: Int
}

object IntegerWrapper extends (Int => IntegerWrapper) {
  private case class IntegerWrapperImpl(i: Int) extends IntegerWrapper

  private val instances: List[IntegerWrapperImpl] = ...
    /* Wrapper instances for i in [0..N) */

  def apply(i: Int): IntegerWrapper = instances(i)

  def unapply(iw: IntegerWrapper): Option[Int] = Some(iw.i)
}

The advantage is that equals and hashCode are still generated by the compiler, since IntegerWrapperImpl is a case class. The disadvantage is that you cannot directly use other compiler-added case class goodies, e.g., copy. If you want to use that, either expose IntegerWrapperImpl to the client, or, IMHO better, add copy to the IntegerWrapper interface.

Pattern matching works as usual:

val iw0 = IntegerWrapper(0)
val iw1: IntegerWrapper = IntegerWrapper(1)

iw0 match {
  case IntegerWrapper(0) => println("IW(0)")
  case _ => println("something else")
} // IW(0)

iw1 match {
  case IntegerWrapper(1) => println("IW(1)")
  case _ => println("something else")
} // IW(1)

iw1 match {
  case IntegerWrapper(2) => println("IW(2)")
  case _ => println("something else")
} // something else

Upvotes: 2

missingfaktor
missingfaktor

Reputation: 92056

Memoize the results! With Scala, you can even abstract away all the required logic.

The infrastructure you need:

scala> :paste
// Entering paste mode (ctrl-D to finish)

// A little memoization utility

object Memoization extends Memoization

trait Memoization {
  trait Memoizable[A] {
    def memoize(fun: A): A
  }

  implicit def fun1memoizable[A, B] = new Memoizable[A => B] {
    def memoize(f: A => B): (A => B) = new MemoizedFunction(f)
  }

  implicit def fun2memoizable[A, B, C] = new Memoizable[(A, B) => C] {
    def memoize(f: (A, B) => C): (A, B) => C = {
      val memo = new MemoizedFunction(f.tupled)
      (a, b) => memo((a, b))
    }
  }

  implicit def fun3memoizable[A, B, C, D] = new Memoizable[(A, B, C) => D] {
    def memoize(f: (A, B, C) => D): (A, B, C) => D = {
      val memo = new MemoizedFunction(f.tupled)
      (a, b, c) => memo((a, b, c))
    }
  }

  def memoize[F](f: F)(implicit m: Memoizable[F]): F = m memoize f

  class MemoizedFunction[-A, +B](f: A => B) extends (A => B) {
    private[this] val cache = collection.mutable.Map.empty[A, B]
    def apply(a: A): B = cache.getOrElseUpdate(a, f(a))
  }
}

import Memoization._

// Abstracting flyweight pattern 
// (http://en.wikipedia.org/wiki/Flyweight_pattern)
trait Flyweight[Args, Obj] { self: (Args => Obj) =>
  val on: Args => Obj = memoize(this : (Args => Obj))
}

// Ctrl+D

Usage:

scala> :paste
// Entering paste mode (ctrl-D to finish)

case class IntegerWrapper private(i: Int) {
  println(this.toString + " created.")
}

object IntegerWrapper extends (Int => IntegerWrapper) 
  with Flyweight[Int, IntegerWrapper]

// Ctrl+D

scala> IntegerWrapper.on(11)
IntegerWrapper(11) created.
res0: IntegerWrapper = IntegerWrapper(11)

scala> IntegerWrapper.on(11)
res1: IntegerWrapper = IntegerWrapper(11)

This is a general solution and uses a Map. You might be better off using a Vector for your particular case.

Upvotes: 4

Steve
Steve

Reputation: 3068

If you're just trying to avoid allocations, perhaps what you want is a value class? In Scala 2.10, if your IntegerWrapper class extends AnyVal, instances usually won't get allocated, and instead static methods will be called on the value itself. For example:

scala> case class IntegerWrapper(val i: Int) extends AnyVal { def increment = i + 1 }
defined class IntegerWrapper

scala> object Test { IntegerWrapper(2).increment }
defined module Test

scala> :javap -verbose Test
...    
public Test$();
  Code:
   Stack=2, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #13; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   putstatic   #15; //Field MODULE$:LTest$;
   8:   getstatic   #20; //Field IntegerWrapper$.MODULE$:LIntegerWrapper$;
   11:  iconst_2
   12:  invokevirtual   #24; //Method IntegerWrapper$.extension$increment:(I)I
   15:  pop
   16:  return

Note that the extension method being called there is Int => Int.

For comparison, here's what you get if you don't extend AnyVal:

scala> :javap -verbose Test
...
public Test$();
  Code:
   Stack=3, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #13; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   putstatic   #15; //Field MODULE$:LTest$;
   8:   new #17; //class IntegerWrapper
   11:  dup
   12:  iconst_2
   13:  invokespecial   #20; //Method IntegerWrapper."<init>":(I)V
   16:  invokevirtual   #24; //Method IntegerWrapper.increment:()I
   19:  pop
   20:  return

With this version, you can see both the object allocation, and the call to a method of the new IntegerWrapper instance.

Upvotes: 6

R&#233;gis Jean-Gilles
R&#233;gis Jean-Gilles

Reputation: 32719

This is basically the same use case as for scala's Symbol class. So you could have a look at Symbol.scala as an inspiration for a reasonable implementation (in particular, an implementation that won't keep the IntegerWrapper instances in memory forever, even when not needed).

See https://lampsvn.epfl.ch/trac/scala/browser/scala/tags/R_2_9_1_final/src//library/scala/Symbol.scala#L1

Upvotes: 3

Related Questions