Scala: абстрактные типы и дженерики - PullRequest
232 голосов
/ 20 июля 2009

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

Например,

abstract class Buffer {
  type T
  val element: T
}

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

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

Ответы [ 3 ]

241 голосов
/ 20 июля 2009

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

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

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


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

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

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

  • параметризация и
  • абстрактные члены.

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

Путь Скалы

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

Почему?

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

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

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

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


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

Считаете ли вы, что это краткое изложение:

  • Абстрактные типы используются в отношениях «имеет-а» или «использует-а» (например, 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 ограничено верхней границей типа , состоящей из упорядоченного имени класса и уточнения { type O = T }.
Верхняя граница ограничивает специализации T в подклассах теми подтипами Ordered, для которых член типа O из equals T.
Из-за этого ограничения метод < класса Ordered гарантированно применим к получателю и аргументу типа T.
Пример показывает, что член ограниченного типа может сам по себе отображаться как часть границы.
(т.е. Scala поддерживает F-ограниченный полиморфизм )

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

Два лица одинаковых монет

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

  • параметризация и
  • абстрактные члены.

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

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

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

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

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

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

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


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

Элементы абстрактного типа и параметры общего типа в Scala (Билл Веннерс)

(акцент мой)

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

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

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

// 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», но они могут, по крайней мере, получить имя типа, не заглядывая в документацию.

37 голосов
/ 20 июля 2009

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

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

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

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

18 голосов
/ 05 июня 2012

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

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

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)
}

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

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

...