Scala: абстрактные типы против дженериков
Я читал Тур по Скале: Абстрактные типы. Когда лучше использовать абстрактные типы?
Например,
abstract class Buffer {
type T
val element: T
}
скорее, что дженерики, например,
abstract class Buffer[T] {
val element: T
}
5 ответов
У вас есть хорошая точка зрения по этому вопросу здесь:
Цель системы типов 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 ограничено верхней границей типа, которая состоит из имени класса Ordered и уточнения
{ type O = T }
,
Верхняя граница ограничивает специализации T в подклассах теми подтипами Ordered, для которых член типа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
это тип "параметра прибора".
Им все еще нужно будет выяснить, что означает "параметр фикстуры", но они могут по крайней мере получить имя типа, не заглядывая в документацию.
У меня был тот же вопрос, когда я читал о Scala.
Преимущество использования дженериков в том, что вы создаете семейство типов. Никто не будет нуждаться в подклассе Buffer
- они могут просто использовать Buffer[Any]
, Buffer[String]
, так далее.
Если вы используете абстрактный тип, то люди будут вынуждены создать подкласс. Людям понадобятся такие занятия, как AnyBuffer
, StringBuffer
, так далее.
Вы должны решить, что лучше для ваших конкретных потребностей.
Вы можете использовать абстрактные типы в сочетании с параметрами типов для создания пользовательских шаблонов.
Предположим, вам нужно установить паттерн с тремя связанными чертами:
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)
}
Таким образом, как абстрактные типы, так и параметры типов используются для создания абстракций. У них обоих есть слабые и сильные стороны. Абстрактные типы более конкретны и способны описывать любую структуру типов, но являются подробными и требуют явного указания. Параметры типа могут создавать множество типов мгновенно, но вам нужно больше беспокоиться о наследовании и границах типов.
Они дают синергию друг другу и могут использоваться совместно для создания сложных абстракций, которые не могут быть выражены только одним из них.
Стивен Компалл. Члены типа — это (почти) параметры типа https://typelevel.org/blog/2015/07/13/type-members-parameters.html .
Джон Претти @propensive. Члены типа и параметры типа — NE Scala 2016 https://www.youtube.com/watch?v=R8GksuRw3VI
Параметры типа можно сделать членами типа.
trait A[_T] {
type T = _T
}
Члены типа могут быть сделаны параметрами типа.
trait A { type T }
object A {
type Aux[_T] = A { type T = _T }
}
// using A.Aux[T] instead of A[T]
Но:
- Члены типа не могут использоваться в первичном конструкторе и самотипе.
{ self: T => ...
. Параметры типа не могут напрямую вызываться в экземплярах. Члены типа можно рассматривать как параметры именованного типа.
trait A[T]
val a: A[Int] = ???
type X = ?? // what is T of a?
trait A { type T }
val a: A { type T = Int } = ???
type X = a.T
trait A[_T] { type T = _T }
val a: A[Int] = ???
type X = a.T
- Вариантность членов типа не может быть объявлена на сайте определения, только на сайте вызова (например, для параметров типа в Java).
trait A[+T] // definition
trait A[-T] // definition
trait A[T]
type X[+T] = A[_ <: T] // call
type Y[-T] = A[_ >: T] // call
trait A { type T }
type X[+_T] = A { type T <: _T } // call
type Y[-_T] = A { type T >: _T } // call
- Разница заключается в частичном применении. Для
trait MyTrait { type A; type B; type C }
вы можете указать некоторые типы и не указывать другие. Но по чертеMyTrait[A, B, C]
вы можете либо указать их все, либо не указывать ни одного из них. Таким образом, параметры типа больше похожи на входные данные (которые необходимо указать), а члены типа больше похожи на выходные данные (которые должны быть выведены).
Когда в Shapeless необходимы зависимые типы?
Почему для вычислений на уровне типа необходим метод Aux?
- Существуют различия в выводе типа для параметров типа и членов типа. Параметр типа должен быть выведен, а если его невозможно вывести, он минимизируется (например,
Nothing
). Но член типа может оставаться абстрактным.
В чем разница между «def apply[T](c:T)» и «type T;def apply(c:T)»
Использование шаблона Aux компилируется без определения соответствующего типа.
Почему Scala выводит нижний тип, если параметр типа не указан?( отвечать )
- Также могут быть различия в неявном разрешении для классов типов типа-параметра и типа-члена (функциональные зависимости, т.е. в
trait A[T] { type S }
тип иногда может зависеть от функционального состоянияtrait A[T, S]
типыT
,S
произвольны,class A t s | t -> s
илиclass A t where type S t
против.class A t s
в Хаскеле).
https://github.com/lampepfl/dotty/issues/17212
https://github.com/scala/bug/issues/12767
- Кроме того, обобщенные алгебраические типы типа-параметра и типа-члена (GADT) различны.
Scala 3. Реализация зависимого типа функции
https://github.com/lampepfl/dotty/issues/17235
- Как в Scala 3 параметры типа собирались кодировать как члены типа и почему в итоге решили этого не делать
https://dotty.epfl.ch/docs/internals/higher-kinded-v2.html
https://contributors.scala-lang.org/t/scala-3-type-parameters-and-type-members/3472
Думаю, здесь нет большой разницы. Абстрактные члены типа можно рассматривать как просто экзистенциальные типы, которые похожи на типы записей в некоторых других функциональных языках.
Например, у нас есть:
class ListT {
type T
...
}
а также
class List[T] {...}
потом ListT
точно так же, как List[_]
. Удовлетворение членов типа состоит в том, что мы можем использовать класс без явного конкретного типа и избегать слишком большого количества параметров типа.