Scala: абстрактные типы против дженериков

Я читал Прогулка по Scala: Абстрактные типы. Когда лучше использовать абстрактные типы?

Например,

abstract class Buffer {
  type T
  val element: T
}

скорее, что дженерики, например,

abstract class Buffer[T] {
  val element: T
}

Ответ 1

У вас есть хорошая точка зрения на эту проблему здесь:

Цель Scala Тип системы
Разговор с Мартином Одерским, часть III
Билл Веннерс и Фрэнк Соммерс (18 мая 2009 г.)

Обновление (октябрь 2009 г.): то, что следует ниже, действительно было проиллюстрировано в этой новой статье Билла Веннера:
Абстрактные члены-члены по сравнению с параметрами общего типа в Scala (см. резюме в конце)


(Вот соответствующий отрывок из первого интервью, май 2009 года, акцент мой)

Общий принцип

Всегда существовало два понятия абстракции:

  • и
  • абстрактные члены.

В Java у вас также есть и то, и другое, но это зависит от того, что вы абстрагируете. В Java у вас есть абстрактные методы, но вы не можете передать метод в качестве параметра.
У вас нет абстрактных полей, но вы можете передать значение в качестве параметра.
И аналогичным образом у вас нет членов абстрактного типа, но вы можете указать тип как параметр.
Итак, на Java у вас также есть все три из них, но есть разница в том, какой принцип абстракции вы можете использовать для каких-то вещей. И вы можете утверждать, что это различие довольно произвольно.

Scala Путь

Мы решили использовать те же принципы построения для всех трех типов членов.
Таким образом, вы можете иметь абстрактные поля, а также значения параметров.
Вы можете передавать методы (или "функции" ) в качестве параметров, или вы можете абстрагироваться от них.
Вы можете указать типы как параметры, или вы можете абстрагироваться от них.
И то, что мы получаем концептуально, состоит в том, что мы можем моделировать одно с точки зрения другого. По крайней мере, в принципе, мы можем выразить всякую параметризацию как форму объектно-ориентированной абстракции. Таким образом, в некотором смысле вы могли бы сказать, что Scala является более ортогональным и полным языком.

Почему?

Что, в частности, абстрактные типы, которые вы покупаете, - это хорошее обращение к этим ковариационным проблемам, о которых мы говорили раньше.
Одной из стандартных проблем, которая существует уже давно, является проблема животных и продуктов.
Задача заключалась в том, чтобы иметь класс Animal с методом eat, который ест немного еды.
Проблема в том, что если мы подклассируем Animal и имеем класс, такой как Cow, тогда они будут есть только траву, а не произвольную пищу. Корова не могла есть Рыбу, например.
То, что вы хотите, - это сказать, что у корова есть метод еды, который ест только Траву, а не другие вещи.
На самом деле, вы не можете сделать это на Java, потому что получается, что вы можете создавать несостоятельные ситуации, например проблему назначения Fruit для переменной Apple, о которой я говорил ранее.

Ответ: вы добавляете абстрактный тип в класс Animal.
Вы говорите, мой новый класс Animal имеет тип SuitableFood, которого я не знаю.
Так что это абстрактный тип. Вы не выполняете реализацию этого типа. Тогда у вас есть метод eat, который ест только SuitableFood.
И затем в классе Cow я бы сказал: "Хорошо, у меня есть корова, которая расширяет класс Animal, а для Cow type SuitableFood equals Grass.
Таким образом, абстрактные типы предоставляют это понятие типа в суперклассе, который я не знаю, который затем заполняю позже в подклассах тем, что я знаю.

То же самое с параметризацией?

Действительно, вы можете. Вы можете параметризовать класс Animal с пищей, которую он ест.
Но на практике, когда вы делаете это со многими другими вещами, это приводит к взрыву параметров, и обычно, что еще, в границах параметров.
В 1998 ECOOP, Ким Брюс, Фил Вадлер, и у меня была бумага, где мы показали, что , поскольку вы увеличиваете количество вещей, которые не знаете, типичная программа будет расти квадратично.
Поэтому есть очень хорошие причины не делать параметры, но иметь этих абстрактных членов, потому что они не дают вам этот квадратичный взрыв.


thatismatt спрашивает в комментариях:

Считаете ли вы следующее справедливое резюме:

  • Абстрактные типы используются в отношениях "has-a" или "uses-a" (например, a Cow eats Grass)
  • где в качестве общих элементов обычно относятся "отношения" (например, List of Ints)

Я не уверен, что взаимоотношение отличается от использования абстрактных типов или дженериков. Другое дело:

  • как они используются, и
  • как управлять границами параметров.

Чтобы понять, о чем говорит Мартин, когда речь заходит о "взрыве параметров и, как правило, о том, что еще в границах параметров" и о его последующем квадратичном росте при моделировании абстрактного типа с использованием дженериков, вы можете рассмотреть статью " Масштабируемая абстракция компонентов", написанная... Мартином Одерским и Маттиасом Ценгером для OOPSLA 2005, упоминается в публикациях проекта Palcom (завершен в 2007 году).

Соответствующие выдержки

Определение

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

(Примечание. Семейный полиморфизм был предложен для объектно-ориентированных языков как решение для поддержки многоразовых, но безопасных по типу взаимно-рекурсивных классов.
Ключевой идеей семейного полиморфизма является понятие семейств, которые используются для группировки взаимно-рекурсивных классов)

абстракция ограниченного типа

abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}

Здесь объявление типа типа T ограничено строкой верхнего уровня, которая состоит из имени класса Ordered и уточнения { type O = T }.
Верхняя граница ограничивает специализации T в подклассах тем подтипам Ordered, для которых член типа O of equals T.
Из-за этого ограничения метод < класса Ordered гарантированно применим к получателю и аргументу типа T.
Пример показывает, что член ограниченного типа может сам отображаться как часть границы.
(т.е. Scala поддерживает F-ограниченный полиморфизм)

(Отметьте, от Петра Коннинга, Уильяма Кука, Вальтера Хилла, Вальтера Олтоффа:
Ограниченная количественная оценка была введена Карделли и Вегнером как средство типизации функций, которые действуют равномерно по всем подтипам данного типа. Они определили простую "объектную" модель и использовали ограниченное количественное определение функций проверки типов, которые имеют смысл для всех объектов, имеющих заданный набор "атрибутов".
Более реалистичное представление объектно-ориентированных языков позволило бы объектам, которые являются элементами рекурсивно определенных типов.
В этом контексте ограниченная количественная оценка больше не служит его намеченной цели. Легко найти функции, которые имеют смысл для всех объектов, имеющих определенный набор методов, но которые не могут быть введены в систему Карделли-Вегнера.
Чтобы обеспечить основу для типизированных полиморфных функций в объектно-ориентированных языках, мы вводим F-ограниченное квантификацию)

Две грани одних и тех же монет

Существуют две основные формы абстракции в языках программирования:

  • и
  • абстрактные члены.

Первая форма типична для функциональных языков, тогда как вторая форма обычно используется в объектно-ориентированных языках.

Традиционно Java поддерживает параметризацию значений и абстракцию членов для операций. Более поздняя версия Java 5.0 с generics поддерживает параметризацию также для типов.

Аргументы для включения дженериков в Scala являются двукратными:

  • Во-первых, кодирование в абстрактные типы не так просто сделать вручную. Помимо потери в лаконичности, есть также проблема случайного имени конфликты между именами абстрактного типа, которые эмулируют параметры типа.

  • Во-вторых, дженерики и абстрактные типы обычно выполняют разные роли в программах Scala.

    • Generics обычно используются, когда требуется только тип экземпляра, тогда как
    • абстрактные типы обычно используются, когда нужно ссылаться на абстрактный введите код клиента.
      Последнее возникает, в частности, в двух ситуациях:
    • Можно было бы скрыть точное определение члена типа из кода клиента, чтобы получить своего рода инкапсуляцию, известную из систем модулей в стиле SML.
    • Или можно было бы переопределить тип, ковариантно в подклассах, чтобы получить семейный полиморфизм.

В системе с ограниченным полиморфизмом переписывание абстрактного типа в дженерики может повлечь квадратичное расширение ограничений типа .


Обновление за октябрь 2009 г.

Абстрактные члены типа против общих параметров типа в Scala (Bill Venners)

(акцент мой)

Мое наблюдение до сих пор о абстрактных членах состоит в том, что они в первую очередь являются лучшим выбором, чем параметры типового типа, когда:

  • вы хотите, чтобы люди смешивались в определениях этих типов с помощью признаков.
  • Вы считаете, что явное упоминание имени члена типа, когда оно определено, поможет прочитать код.

Пример:

если вы хотите передать в тестовые объекты три разных объекта прибора, вы сможете это сделать, но вам нужно указать три типа: по одному для каждого параметра. Таким образом, если бы я применил подход типа параметра, классы классов могли бы выглядеть следующим образом:

// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
  // ...
}

В то время как при подходе типа элемента это будет выглядеть так:

// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
  // ...
}

Еще одно незначительное различие между членами абстрактного типа и параметрами родового типа заключается в том, что когда задан параметр универсального типа, считыватели кода не видят имя параметра типа. Таким образом, кто-то видел эту строку кода:

// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
  // ...
}

Они не знали бы, что имя параметра типа, заданного как StringBuilder, не выглядело. В то время как имя параметра типа находится прямо там, в коде в подходе типа абстрактного типа:

// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
  type FixtureParam = StringBuilder
  // ...
}

В последнем случае читатели кода могли видеть, что StringBuilder - это параметр "параметр приспособления".
Им все равно нужно будет выяснить, что означает параметр "fixture", но они могут, по крайней мере, получить имя типа, не заглядывая в документацию.

Ответ 2

У меня был тот же вопрос, когда я читал о Scala.

Преимущество использования дженериков заключается в том, что вы создаете семейство типов. Никто не будет нуждаться в подклассе Buffer - они могут просто использовать Buffer[Any], Buffer[String] и т.д.

Если вы используете абстрактный тип, люди будут вынуждены создавать подкласс. Людям нужны классы типа AnyBuffer, StringBuffer и т.д.

Вам нужно решить, что лучше для вашей конкретной потребности.

Ответ 3

Вы можете использовать абстрактные типы в сочетании с параметрами типа для создания пользовательских шаблонов.

Предположим, вам нужно установить шаблон с тремя связанными признаками:

trait AA[B,C]
trait BB[C,A]
trait CC[A,B]

в том смысле, что аргументы, упомянутые в параметрах типа, почти равны AA, BB, CC

Вы можете найти какой-то код:

trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]

который не работал бы этим простым способом из-за связей типа параметра. Вы должны сделать это ковариантным, чтобы правильно наследовать.

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]

Этот один образец будет компилироваться, но он устанавливает сильные требования к правилам дисперсии и не может использоваться в некоторых случаях

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
  def forth(x:B):C
  def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
  def forth(x:C):A
  def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
  def forth(x:A):B
  def back(x:B):A
}

Компилятор будет обрабатывать ошибки проверки ошибок

В этом случае вы можете собрать все требования типа в дополнительном признаке и параметризовать другие черты над ним

//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
  type A <: AA[O]
  type B <: BB[O]
  type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:B):C
  def right(r:C):B = r.left(this)
  def join(l:B, r:C):A
  def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:C):A
  def right(r:A):C = r.left(this)
  def join(l:C, r:A):B
  def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:A):B
  def right(r:B):A = r.left(this)
  def join(l:A, r:B):C
  def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}

Теперь мы можем написать конкретное представление для описанного шаблона, определить методы left и join во всех классах и получить право и дважды бесплатно

class ReprO extends OO[ReprO] {
  override type A = ReprA
  override type B = ReprB
  override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
  override def left(l:B):C = ReprC(data - l.data)
  override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
  override def left(l:C):A = ReprA(data - l.data)
  override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
  override def left(l:A):B = ReprB(data - l.data)
  override def join(l:A, r:B):C = ReprC(l.data + r.data)
}

Итак, оба абстрактных типа и параметры типа используются для создания абстракций. Оба они имеют слабую и сильную точку. Абстрактные типы более специфичны и способны описывать любую структуру типов, но являются подробными и требуют явного указания. Параметры типа могут создавать кучу типов мгновенно, но вы можете дополнительно беспокоиться о наследовании и ограничениях типов.

Они дают синергию друг другу и могут использоваться совместно для создания сложных абстракций, которые не могут быть выражены только одним из них.