Чтение Scala в Примере, пытаясь понять философию позади примеров - PullRequest
3 голосов
/ 20 декабря 2011

Я читаю книгу Scala in examples, и почти каждый пример там имеет следующую конструкцию:

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 ]

5 голосов
/ 20 декабря 2011

1) Да, обычно есть отдельные классы для пустых и непустых контейнеров, это обычно называется алгебраической структурой данных, однако обычно это не так очевидно. Например, в Scala's List есть два класса: Nil для представления пустого списка и ::, который содержит один элемент и другой список. Так

List(1,2,3)

хотя обычно упоминается чертой List[T], на самом деле это экземпляр :: [B] (hd: B, tl: List[B]), который выглядит следующим образом:

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

2) Каждый класс должен реализовывать метод isEmpty, так как, если вы заметили, значение отличается в каждом дочернем классе. Он просто сохраняет некоторые вычисления, чтобы выяснить, является ли экземпляр Stack пустым или нет, поскольку каждый дочерний тип уже знает это во время компиляции.

3 голосов
/ 20 декабря 2011

1) Да, это способ выражения Scala алгебраических типов данных или различимых объединений , распространенных в функциональных языках программирования. Альтернативой здесь является наличие только одного класса с необязательными членами данных (либо с использованием Option, который также имеет подкласс для пустого и подкласс для непустого, либо с использованием null). Это заставляет все ваши методы проверять, есть ли у объекта данные или нет, делая их более сложными; использование подклассов имеет системную диспетчеризацию виртуального метода (что он собирается делать в любом случае), сделайте эту проверку за вас. Это также заставляет представленные данные быть непротиворечивыми ; Stack либо имеет elem и rest (NonEmptyStack), либо не имеет (EmptyStack). У него не может быть одного, но нет другого (при условии, что никто намеренно не делает NonEmptyStack с null, что очень редко встречается в Scala).

Широко применима общая схема типов данных, являющаяся одним из нескольких случаев, когда к каждому случаю прилагаются разные данные. Наличие в одном из случаев данных - это просто тривиальный случай этого общего паттерна. Как программист Scala, использование этого общего шаблона станет вам знакомым, поэтому вполне естественно применять его и к простым случаям.


2) Вы заметите, что все методов в каждом дочернем классе немедленно возвращают значение без дальнейших вычислений (исключая случаи ошибок, которые немедленно генерируют исключение без дальнейших вычислений). Это делает их очень очевидными и простыми для понимания, если вы привыкли мыслить с точки зрения диспетчеризации виртуальных методов.

Кроме того, это делает их весьма эффективными; вычисление only , необходимое для определения того, что должен возвращать каждый метод, - это отправка виртуального метода, которую система все равно сделает для вас. Чтобы реализовать isEmpty в родительском классе, вы должны добавить некоторую форму проверки и ветвления экземпляра; в любом случае это всего лишь ручная форма отправки виртуальных методов системы!

Более того, и я думаю, что самое важное, реализация дочернего класса более удобна в обслуживании. Допустим, вы добавили еще один специализированный вид непустого стека (возможно, у вас есть множество стеков с ровно одним элементом и вы не хотите тратить пространство на хранение дополнительной ссылки на пустой стек или что-то в этом роде). Если у вас есть ответвление в родительском элементе для возврата ответов, которые различны для разных подклассов, вы должны перейти и обновить каждый из них, чтобы учесть новый подкласс. И компилятор, вероятно, не заметит, если вы этого не сделаете. Если вы реализовали каждое специфичное для подкласса поведение в подклассе, то вы просто реализуете методы в новом подклассе.

2 голосов
/ 20 декабря 2011

Похоже на реализацию List.Я просто догадываюсь здесь, но пример, вероятно, будет продолжен в последующих главах книги, где объясняется сопоставление с образцом, поэтому реализации будут иметь префикс с ключевым словом case, чтобы вы могли сопоставлять на Stack s, как вы можетесовпадение на List с case head :: tail =>.

1 голос
/ 20 декабря 2011

Ну, как бы вы предложили написать это без "пустого" подкласса?Попробуйте написать это (и помните, что вы не можете использовать изменяемое состояние), и вы увидите, что это очень сложно.Единственный известный мне способ требует частного конструктора.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...