【问题标题】:Reading Scala In Example, trying to understand the philosophy behind the examples在示例中阅读 Scala,试图理解示例背后的哲学
【发布时间】:2012-01-23 21:56:21
【问题描述】:

我正在阅读示例书中的 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”,而且对我来说,在父类中这样做更明智?

我想了解这里所涉及的最深刻的哲学。

【问题讨论】:

  • 谢谢,看来这个部门可以追溯到函数式或更准确地说是“面向列表”编程的基石之一,我很久以前在简要接触 Erlang、Clojure 和口齿不清。所以这一定有一个非常重要的意义。至于第二个问题,感谢您的回答,我明白这只是一个太简单的例子,无法见证这种方法的全部力量。
  • 这是常见的做法吗?集合 - 也许,一般应用程序编码,我改为使用 Option。对我来说,你的两个问题都有一个答案——作者的目的是演示如何使用继承,特别是抽象类和方法。 isEmpty、top、pop被演示为抽象方法,因此必须通过继承类来实现,并且可以有不同的实现。这只是一个意见,所以不张贴作为答案。希望对您有所帮助!
  • 此外,这个除法可以帮助您回答如下问题:“如果我将一个空堆栈传递给它,如何编写一个在堆栈上运行但在 编译时失败的函数"
  • 在父级和子级中实现isEmpty(或类似方法)之间存在权衡。将实现推迟到子类的一个好处是,如果您希望添加新的子类,则不必修改父类的代码。

标签: class scala inheritance methods


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

    List(1,2,3)
    

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

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

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        1) 是的,这是 Scala 表达代数数据类型可区分联合的方式,在函数式编程语言中很常见。这里的替代方法是只有一个具有可选数据成员的类(使用Option,它也有一个空子类和一个非空子类,或者使用null)。这迫使您的所有方法都必须检查对象是否确实有数据,从而使它们变得更加复杂;使用子类有系统的虚拟方法调度(无论如何它都会这样做)为你做这个检查。它还强制呈现的数据一致Stack 要么有 elemrest (NonEmptyStack),要么都没有 (EmptyStack)。它不可能有一个但没有另一个(假设没有人故意用null 制作NonEmptyStack,这在Scala 中非常罕见)。

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


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

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

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

        【讨论】:

          猜你喜欢
          • 2015-04-14
          • 1970-01-01
          • 1970-01-01
          • 2012-12-06
          • 1970-01-01
          • 2021-01-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多