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

Ответы:

257

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

Цель системы типов 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 спрашивает в комментариях:

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

(акцент мой)

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

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

Пример:

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

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

VonC
источник
61
Как я могу получить очки кармы, отвечая на вопросы Скалы, когда вы приходите и делаете это ??? :-)
Даниэль С. Собрал
7
Привет, Дэниел: Я думаю, что должны быть конкретные примеры, иллюстрирующие преимущества абстрактных типов над параметризацией. Размещение некоторых в этой теме было бы хорошим началом;) Я знаю, что приветствовал бы это.
VonC
1
Считаете ли вы, что следующее краткое изложение является следующим: абстрактные типы используются в отношениях «имеет-а» или «использует-а» (например, корова ест траву), где в качестве обобщений обычно используются отношения «из» (например, список целых)
thatismatt
Я не уверен, что отношения отличаются между использованием абстрактных типов или обобщений. Отличается то, как они используются и как управляются границы параметров. Больше в моем ответе через мгновение.
VonC
1
Примечание для себя: см. Также этот блог в мае 2010 года: daily-scala.blogspot.com/2010/05/…
VonC
37

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

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

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

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

Даниэль Янковский
источник
18
ммм значительно улучшился в этом плане, вы можете просто потребовать Buffer { type T <: String }или в Buffer { type T = String }зависимости от ваших потребностей
Эдуардо Пареджа Тобес
21

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

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

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

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

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

ayvango
источник
0

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

Например, у нас есть:

class ListT {
  type T
  ...
}

и

class List[T] {...}

Тогда ListTточно так же, как List[_]. Удобство членов типа в том, что мы можем использовать класс без явного конкретного типа и избежать слишком большого количества параметров типа.

Comcx
источник