简体   繁体   中英

Why do each new instance of case classes evaluate lazy vals again in Scala?

From what I have understood, scala treats val definitions as values. So, any instance of a case class with same parameters should be equal. But,

case class A(a: Int) {
   lazy val k = {
       println("k")
       1
   }

 val a1 = A(5)
 println(a1.k)
 Output:
 k
 res1: Int = 1

 println(a1.k)
 Output:
 res2: Int = 1

 val a2 = A(5)
 println(a1.k)
 Output:
 k
 res3: Int = 1

I was expecting that for println(a2.k), it should not print k. Since this is not the required behavior, how should I implement this so that for all instances of a case class with same parameters, it should only execute a lazy val definition only once. Do I need some memoization technique or Scala can handle this on its own?

I am very new to Scala and functional programming so please excuse me if you find the question trivial.

Assuming you're not overriding equals or doing something ill-advised like making the constructor args var s, it is the case that two case class instantiations with same constructor arguments will be equal. However, this does not mean that two case class instantiations with same constructor arguments will point to the same object in memory:

case class A(a: Int)
A(5) == A(5)  // true, same as `A(5).equals(A(5))`
A(5) eq A(5)  // false

If you want the constructor to always return the same object in memory, then you'll need to handle this yourself. Maybe use some sort of factory:

case class A private (a: Int) {
  lazy val k = {
    println("k")
    1
  }
}

object A {
  private[this] val cache = collection.mutable.Map[Int, A]()
  def build(a: Int) = {
    cache.getOrElseUpdate(a, A(a))
  }
}

val x = A.build(5)
x.k                  // prints k
val y = A.build(5)
y.k                  // doesn't print anything
x == y               // true
x eq y               // true

If, instead, you don't care about the constructor returning the same object, but you just care about the re-evaluation of k , you can just cache that part:

case class A(a: Int) {
  lazy val k = A.kCache.getOrElseUpdate(a, {
    println("k")
    1
  })
}

object A {
  private[A] val kCache = collection.mutable.Map[Int, Int]()
}

A(5).k     // prints k
A(5).k     // doesn't print anything

The trivial answer is "this is what the language does according to the spec". That's the correct, but not very satisfying answer. It's more interesting why it does this.

It might be clearer that it has to do this with a different example:

case class A[B](b: B) {
   lazy val k = {
       println(b)
       1
   }
}

When you're constructing two A 's, you can't know whether they are equal, because you haven't defined what it means for them to be equal (or what it means for B's to be equal). And you can't statically intitialize k either, as it depends on the passed in B .

If this has to print twice, it would be entirely intuitive if that would only be the case if k depends on b , but not if it doesn't depend on b .

When you ask

how should I implement this so that for all instances of a case class with same parameters, it should only execute a lazy val definition only once

that's a trickier question than it sounds. You make "the same parameters" sound like something that can be known at compile time without further information. It's not, you can only know it at runtime.

And if you only know that at runtime, that means you have to keep all past uses of the instance A[B] alive. This is a built in memory leak - no wonder Scala has no built-in way to do this.

If you really want this - and think long and hard about the memory leak - construct a Map[B, A[B]] , and try to get a cached instance from that map, and if it doesn't exist, construct one and put it in the map.

I believe case class es only consider the arguments to their constructor (not any auxiliary constructor) to be part of their equality concept. Consider when you use a case class in a match statement, unapply only gives you access (by default) to the constructor parameters.

Consider anything in the body of case classes as "extra" or "side effect" stuffs. I consider it a good tactic to make case classes as near-empty as possible and put any custom logic in a companion object. Eg:

case class Foo(a:Int)

object Foo {
    def apply(s: String) = Foo(s.toInt)
}

In addition to dhg answer, I should say, I'm not aware of functional language that does full constructor memoizing by default. You should understand that such memoizing means that all constructed instances should stick in memory, which is not always desirable.

Manual caching is not that hard, consider this simple code

import scala.collection.mutable

class Doubler private(a: Int) {
  lazy val double = {
    println("calculated")
    a * 2
  }
}

object Doubler{
  val cache = mutable.WeakHashMap.empty[Int, Doubler]
  def apply(a: Int): Doubler = cache.getOrElseUpdate(a, new Doubler(a))
}

Doubler(1).double   //calculated

Doubler(5).double   //calculated

Doubler(1).double   //most probably not calculated

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM