阅读Scala在例子中,试图理解这些例子背后的哲学

时间:2011-12-19 20:11:41

标签: class scala inheritance methods

我正在阅读示例书中的Scala,几乎每个例子都有以下结构:

abstract class Stack[A] {
  def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)
  def isEmpty: Boolean
  def top: A
  def pop: Stack[A]
}
class EmptyStack[A] extends Stack[A] {
  def isEmpty = true
  def top = error("EmptyStack.top")
  def pop = error("EmptyStack.pop")
}
class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] {
  def isEmpty = false
  def top = elem
  def pop = rest
}

我有以下两个相互关联的问题: 1)将空元素和非空元素表示为单独的类是一种常见的Scala实践吗?如果是,那为什么呢? 2)为什么孩子们在可能的情况下都实现了同样的愚蠢方法'isEmpty',对我来说,在父母班级中这样做更明智?

我想知道这里涉及的最深刻的哲学。

4 个答案:

答案 0 :(得分:5)

1)是的,对于空容器和非空容器都有单独的类,这通常称为代数数据结构,但通常不那么明显。例如,Scala的List有两个类,Nil表示一个空列表,::包含一个元素和另一个列表。所以

List(1,2,3)

虽然通常由List[T]特征引用,但实际上是:: [B] (hd: B, tl: List[B])的实例,如下所示:

::(1, ::(2, ::(3, Nil)))

2)如果您注意到,每个类必须实现isEmpty方法,每个子类的值都不同。它只是保存一些计算来确定Stack的实例是否为空,因为每个子类型在编译时都已知道这一点。

答案 1 :(得分:3)

1)是的,这是Scala表达代数数据类型区分联合的方式,在函数式编程语言中很常见。这里的替代方案是只有一个带有可选数据成员的类(使用Option,它还有一个空子类和一个非空子类,或者使用null)。这迫使你的所有方法必须检查对象是否实际上有数据,使它们更复杂;使用子类具有系统的虚拟方法调度(无论如何它将执行此操作)为您执行此检查。它还强制存在的数据一致; StackelemrestNonEmptyStack),或者没有(EmptyStack)。它不可能有一个而不是另一个(假设没有人故意使用NonEmptyStack null,这在Scala非常罕见)。

数据类型的一般模式是几种情况之一,每种情况都附有不同的数据,可广泛应用。其中一个案例包含 no 数据只是这种一般模式的一个简单案例。作为一名Scala程序员,使用这种通用模式对您来说很熟悉,因此将其应用于简单案例似乎也很自然。


2)你会注意到每个子类中的所有方法会立即返回一个值,没有进一步的计算(除了错误情况,它会立即抛出异常而不进行进一步的计算) 。这使得它们非常明显且易于理解,只要您习惯于考虑虚拟方法调度。

此外,它使它们非常有效;确定每个方法应该返回什么所需的唯一计算是虚拟方法调度,无论如何系统将为您执行此操作。要在父类中实现isEmpty,您必须添加某种形式的实例检查和分支;无论如何,这实际上只是系统虚拟方法调度的手动形式!

此外,我认为最重要的是,子级实现更易于维护。假设您添加了另一种特殊类型的非空堆栈(可能您有大量1个元素的堆栈,并且不希望浪费空间存储对空堆栈的额外引用,或者某些东西)。如果您在父级中有分支以返回不同子类的不同答案,则必须更新每个子类以考虑新的子类。 如果你不这样做,编译器可能不会注意到。如果你已经在子类中实现了每个子类特定的行为,那么你只需在新的子类中实现这些方法。

答案 2 :(得分:2)

它看起来类似于List的实现。我只是在这里猜测,但这个例子可能会在本书的后续章节中继续进行,其中解释了模式匹配,因此实现将以case关键字为前缀,以便您可以在Stack上进行匹配s,因为您可以List方式在case head :: tail =>上匹配。

答案 3 :(得分:1)

那么,如果没有“空”子类,你会如何建议写这个?尝试写它(并记住你不能使用可变状态),你会发现它很难。我所知道的唯一方法需要私有构造函数。