情境:
我想实现一个无限的列表:
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
不允许使用var
或call-by-name
构造函数参数。例如:
class A(val x: =>Int)
//error: `val' parameters may not be call-by-name
但相比之下,正常的构造函数参数仍为val
,尽管private
。例如:
class A(x: =>Int)
// pass
所以问题:
val
或var
有关吗?
call-by-name
的要点是推迟计算。为什么不能推迟val
或var
计算(或初始化)?case class
限制来实现无限列表?答案 0 :(得分:15)
没有矛盾:class A(x: => Int)
相当于class A(private[this] val x: => Int)
而不是class A(private val x: => Int)
。 private[this]
标记值instance-private,而没有进一步指定的private-modifier允许从该类的任何实例访问值。
不幸的是,也不允许定义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
添加到MyNode
' toString
- 方法来进行检查。
编辑:阅读Chris Martin的评论后:equals
的实施也会带来一个问题,可能比toString
的实施更严重(它主要用于调试)和hashCode
(如果你不能考虑参数,这只会导致更高的碰撞率)。您必须仔细考虑如何实施equals
才有意义。
答案 1 :(得分:5)
我也没有找到为什么在案例类中禁止使用确切的名字参数。我想解释应该是非常精细和复杂的。 但是Runar Bjarnason在他的书#34; Functional Programming in Scala"提供了一个很好的方法来处理这个障碍。他使用了" thunk"的概念。连同记忆。 以下是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: _*))
}
}
如您所见,对于案例类数据构造函数而不是常规的by-name参数,他们使用他们称之为" thunk",零参数() => T
的函数。然后为了使用户透明,他们在伴随对象中声明了一个智能构造函数,它允许你提供一个名字参数并让它们被记忆。
答案 2 :(得分:0)
This is actually similar approach to the Stream
solution but simplified to what is actually required:
case class A(x: () => Int) {
lazy val xx = x()
}
So you can use your case class as:
def heavyOperation: Int = ???
val myA = A(heavyOperation)
val myOtherA = A(() => 10)
val useA = myA.xx + myOtherA.xx
Like this the actual heavy operation will be performed only when you use xx
, i.e., only on the last line.
答案 3 :(得分:0)
我喜欢使用隐式函数使 thunk 像按名称调用一样工作。
例如在这个例子中:
case class Timed[R](protected val block: () => R) {
override def toString() = s"Elapsed time: $elapsedTime"
val t0 = System.nanoTime()
val result = block() // execute thunk
val t1 = System.nanoTime()
val elapsedTime = t1 - t0
}
implicit def blockToThunk[R](bl: => R) = () => bl //helps to call Timed without the thunk syntax
这让我们调用 Timed({Thread.sleep(1000); println("hello")}) 例如使用按名称调用语法