Class 阅读示例中的Scala,试图理解示例背后的哲学

Class 阅读示例中的Scala,试图理解示例背后的哲学,class,scala,inheritance,methods,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 = er

我正在阅读《示例中的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”,而在我看来,在父类中这样做可能更明智


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

它看起来类似于
列表的实现。我只是在猜,但是这个例子可能会在本书后面的章节中继续解释,其中解释了模式匹配,因此实现将以
case
关键字作为前缀,这样您就可以在
堆栈上进行匹配,就像您可以在
列表上以
case head::tail=>
的方式进行匹配一样。

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

List(1,2,3)
虽然通常由
List[T]
trait引用,但实际上是
:[B](hd:B,tl:List[B])
的一个实例,看起来像:

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

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

那么,如果没有“空”子类,您打算如何编写这个?试着去写它(记住你不能使用可变状态),你会发现这是非常困难的。我所知道的唯一方法需要一个私有构造函数。

1)是的,这是Scala表达代数数据类型或区分并集的方法,在函数式编程语言中很常见。这里的替代方法是只使用一个带有可选数据成员的类(使用
选项
,该选项也有一个子类表示empty,一个子类表示non-empty,或者使用
null
)。这迫使您的所有方法都必须检查对象是否实际有数据,从而使它们更加复杂;使用子类让系统的虚拟方法分派(无论如何它都会这样做)为您执行此检查。它还强制显示的数据保持一致;
堆栈既有
元素
剩余
非空堆栈
),也没有(
空堆栈
)。它不可能有一个而没有另一个(假设没有人故意用
null
创建
NonEmptyStack
,这在Scala中非常罕见)

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


2) 您将注意到,每个子类中的所有方法都会立即返回一个值,无需进一步计算(错误情况除外,它会立即抛出异常,无需进一步计算)。这使得它们非常明显并且容易理解,只要您习惯于从虚拟方法分派的角度思考

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


此外,我认为最重要的是,子类实现更易于维护。假设您添加了一种更专门的非空堆栈(可能您有一堆正好包含1个元素的堆栈,并且不想浪费空间来存储对空堆栈的额外引用或其他内容)。如果您在父类中有分支以返回不同子类的不同答案,那么您必须去更新每个答案,以考虑新的子类如果您不这样做,编译器可能不会注意到。如果您在子类中实现了每个子类特定的行为,那么您只需在新的子类中实现方法。

谢谢,这看起来就像是函数的基石之一,或者更准确地说,“面向列表”很久以前,我在简要介绍Erlang、Clojure和Lisp时就注意到了编程。所以这肯定有一个非常重要的意义。至于第二个问题,多亏了你的回答,我明白这只是一个太简单的例子,无法证明这种方法的全部威力。这是普遍做法吗?集合-也许,一般的应用程序编码,我使用选项。对我来说,你的两个问题都有一个答案——作者的意思是演示如何使用继承,特别是抽象类和方法。isEmpty、top、pop被演示为抽象方法,因此必须通过继承类来实现,并且可以有不同的实现。这只是一个意见,所以不作为答案张贴。希望有帮助!此外,这个部分可以帮助您回答这样的问题:“如何编写一个在堆栈上运行但在编译时失败的函数,如果我向它传递一个空堆栈”在父级实现
isEmpty
(或类似方法)与