【问题标题】:Scala case class prohibits call-by-name parameters?Scala案例类禁止按名称调用参数?
【发布时间】:2015-01-01 04:40:20
【问题描述】:

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

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是不允许的。

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

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 限制来实现无限列表?

【问题讨论】:

  • 对于一个无限的数据结构,case类sugar提供了什么值? equalshashCodetoString 不起作用。我不确定我对unapply 有什么期望。

标签: scala functional-programming lazy-evaluation callbyname


【解决方案1】:

没有矛盾: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)。我认为这是因为案例类需要访问其他实例的构造函数值,因为它们实现了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")
  }
}

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

编辑: 在阅读了 Chris Martin 的评论后:equals 的实现也会带来一个可能比toString 的实现更严重的问题(主要用于调试)和hashCode(如果不考虑参数,只会导致更高的碰撞率)。您必须仔细考虑如何实现equals 才有意义。

【讨论】:

  • 我想你会按照原来的方式离开equals,但需要注意的是它只适用于终止列表。 Stream 就是这样工作的,对吧? Stream(1) == Stream(1)true,但 Stream.from(1) == Stream.from(1) 不会停止。
  • @ChrisMartin 取决于列表的使用方式(没有循环,很好),但我可能会实施更安全的equals-方法。例如,可以将一组已经访问过的MyList 实例传递给专门的equalsMyList(other: MyList[T], visited: Set[MyList[T]]) 方法。然后,专用方法可以通过检查this 是否已包含在visited 中来检查递归,并在这种情况下返回true。中止标准可能需要检查 object identity 而不是常规相等性,否则我们可能会遇到下一个无限递归。
【解决方案2】:

我也没有找到为什么在案例类中禁止使用名称参数。我想解释应该相当详尽和复杂。 但是 Runar Bjarnason 在他的书“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: _*))
 }
}

如您所见,他们使用所谓的“thunk”,一个零参数() => T 的函数,而不是案例类数据构造函数的常规名称参数。然后为了让用户透明,他们在伴生对象中声明了一个智能构造函数,它允许您提供按名称参数并使其记忆化。

【讨论】:

    【解决方案3】:

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

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

    所以你可以使用你的案例类:

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

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

    【讨论】:

      【解决方案4】:

      我喜欢使用隐式函数使 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")}) 例如使用名称语法调用

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-08-09
        • 2014-04-10
        相关资源
        最近更新 更多