Scala案例类禁止按名称调用参数?

问题描述 投票:24回答:3

场景: 我想实现一个无限的列表:

abstract class MyList[+T]
case object MyNil extends MyList[Nothing]
case class MyNode[T](h:T,t: => MyList[T]) extends MyList[T]

//error: `val' parameters may not be call-by-name

问题: 错误是不允许使用call-by-name

我听说这是因为val不允许使用varcall-by-name构造函数参数。例如:

class A(val x: =>Int) 
//error: `val' parameters may not be call-by-name

但相比之下,正常的构造函数参数仍然是val,尽管private。例如:

class A(x: =>Int) 
// pass

所以问题:

  • 问题真的是关于valvar? 如果是这样,因为call-by-name的要点是推迟计算。为什么不能延迟valvar计算(或初始化)?
  • 如何绕过case class限制来实现无限列表?
scala functional-programming lazy-evaluation callbyname
3个回答
15
投票

没有矛盾:class A(x: => Int)相当于class A(private[this] val x: => Int)而不是class A(private val x: => Int)private[this]标记值实例私有,而没有进一步规范的私有修改器允许从该类的任何实例访问值。

不幸的是,也不允许定义case class A(private[this] val x: => Int)。我假设这是因为case-classes需要访问其他实例的构造函数值,因为它们实现了equals方法。

不过,您可以实现案例类手动提供的功能:

abstract class MyList[+T]

class MyNode[T](val h: T, t: => MyList[T]) extends MyList[T]{

  def getT = t // we need to be able to access t 

  /* EDIT: Actually, this will also lead to an infinite recursion
  override def equals(other: Any): Boolean = other match{
    case MyNode(i, y) if (getT == y) && (h == i) => true
    case _ => false
  }*/

  override def hashCode = h.hashCode

  override def toString = "MyNode[" + h + "]"

}

object MyNode {
  def apply[T](h: T, t: => MyList[T]) = new MyNode(h, t)
  def unapply[T](n: MyNode[T]) = Some(n.h -> n.getT)
}

要检查此代码,您可以尝试:

def main(args: Array[String]): Unit = {
  lazy val first: MyNode[String] = MyNode("hello", second)
  lazy val second: MyNode[String] = MyNode("world", first)
  println(first)
  println(second)
  first match {
    case MyNode("hello", s) => println("the second node is " + s)
    case _ => println("false")
  }
}

不幸的是,我不确定为什么禁止使用call-by-name val和var成员。但是,它至少存在一个危险:考虑案例类如何实现toString;调用每个构造函数值的toString方法。这可能(并且在这个例子中)会导致值无限地调用自己。你可以通过将t.toString添加到MyNodetoString-method来检查这一点。

编辑:阅读克里斯马丁的评论后:equals的实施也将带来一个问题,可能比toString(主要用于调试)和hashCode(如果你能'只会导致更高的碰撞率)更严重t考虑参数)。你必须仔细考虑如何实现equals是有意义的。


5
投票

我还没有找到为什么在案例类中禁止确切的名字参数。我想解释应该是非常精细和复杂的。但是Runar Bjarnason在他的书“Functional Programming in Scala”中提供了一个很好的方法来处理这个障碍。他使用“thunk”的概念和memoizing。以下是Stream实现的示例:

sealed trait Stream[+A]
case object Empty extends Stream[Nothing]
case class Cons[+A](h: () => A, t: () => Stream[A]) extends Stream[A]
object Stream {
 def cons[A](hd: => A, tl: => Stream[A]): Stream[A] = {
  lazy val head = hd
  lazy val tail = tl
  Cons(() => head, () => tail)
 }
 def empty[A]: Stream[A] = Empty
 def apply[A](as: A*): Stream[A] =
  if (as.isEmpty) empty else cons(as.head, apply(as.tail: _*))
 }
}

如您所见,不是case类数据构造函数的常规by-name参数,而是使用他们称之为“thunk”的函数,这是零参数() => T的函数。然后为了使用户透明,他们在伴随对象中声明了一个智能构造函数,它允许您提供一个名称参数并使它们被记忆。


0
投票

这实际上是与Stream解决方案类似的方法,但简化为实际需要的方法:

case class A(x: () => Int) {
  lazy val xx = x()
}

所以你可以使用你的case类:

def heavyOperation: Int = ???
val myA = A(heavyOperation)
val myOtherA = A(() => 10)
val useA = myA.xx + myOtherA.xx

像这样,只有当你使用xx时,才会执行实际的繁重操作,即仅在最后一行。

© www.soinside.com 2019 - 2024. All rights reserved.