函数式编程——为什么每个新的case类实例都会在Scala中再次评估惰性val


根据我的理解,scala将val定义视为值。因此,具有相同参数的case类的任何实例都应该相等。但是,
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

我原以为对于println(a2.k(,它不应该打印k。既然这不是必需的行为,我应该如何实现这一,以便对于具有相同参数的case类的所有实例,它应该只执行一次惰性val定义我需要一些内存化技术吗?或者Scala可以自己处理?

我对Scala和函数式编程很陌生,所以如果你觉得这个问题很琐碎,请原谅我。

假设您没有重写equals或做一些不明智的事情,例如使构造函数参数为var s,那么在这种情况下,具有相同构造函数参数的两个case类实例化将相等。然而,这并不意味着具有相同构造函数参数的两个case类实例化将指向内存中的相同对象

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

如果您希望构造函数总是在内存中返回相同的对象,那么您需要自己处理。也许使用某种工厂:

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

相反,如果您不关心构造函数返回相同的对象,而只关心k的重新评估,则可以缓存该部分:

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

琐碎的答案是"这就是语言根据规范所做的"。这是正确的答案,但不是很令人满意。它为什么这么做更有趣。

更清楚的是,它用另一个例子来做这件事:

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

当你构造两个A时,你不知道它们是否相等,因为你还没有定义它们相等意味着什么(或者B相等意味着哪里(。您也不能静态初始化k,因为它取决于传入的B

如果必须打印两次,如果仅在k依赖于b的情况下是这样,而在不依赖于b的情况下则不是这样,这将是完全直观的。

当你问

我应该如何实现这一点,以便对于具有相同参数的case类的所有实例,它应该只执行一次延迟val定义

这是一个比听起来更棘手的问题。您使"相同的参数"听起来像是在编译时可以知道的东西,而无需进一步的信息。不是,你只能在运行时知道。

如果您只在运行时知道这一点,那么这意味着您必须保持实例A[B]过去的所有使用。这是一个内置的内存泄漏——难怪Scala没有内置的方法来做到这一点。

如果你真的想要这样做——并且仔细考虑内存泄漏——那么就构造一个Map[B, A[B]],并尝试从该映射中获取一个缓存实例,如果它不存在,那么构造一个并将其放入映射中。

我相信case class只认为其构造函数(而不是任何辅助构造函数(的参数是其等式概念的一部分。考虑一下,当您在match语句中使用case类时,unapply只允许您访问(默认情况下(构造函数参数。

将case类主体中的任何内容都视为"额外"或"副作用"。我认为这是一个很好的策略,使case类尽可能接近空,并将任何自定义逻辑放在伴随对象中。例如:

case class Foo(a:Int)
object Foo {
    def apply(s: String) = Foo(s.toInt)
}

除了dhg-answer,我应该说,我不知道函数式语言默认情况下会进行完整的构造函数记忆。你应该明白,这种记忆意味着所有构建的实例都应该保存在内存中,这并不总是可取的。

手动缓存没有那么难,考虑一下这个简单的代码

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

最新更新