Scala: Абстрактні типи проти дженерики


Відповіді:


257

Ви маєте хорошу точку зору на це питання тут:

Мета
розмови системи Scala : Бесіда з Мартіном Одерським, частина III
Біллом Веннерсом та Френком Соммерсом (18 травня 2009 р.)

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


(Ось відповідний уривок першого інтерв'ю, травень 2009 р., Акцент мій)

Загальний принцип

Завжди було два поняття абстракції:

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

У Java у вас теж є обидва, але це залежить від того, над чим ви абстрагуєтесь.
У Java у вас є абстрактні методи, але ви не можете передавати метод як параметр.
У вас немає абстрактних полів, але ви можете передати значення як параметр.
І так само у вас немає абстрактних членів типу, але ви можете вказати тип як параметр.
Тож у Java у вас є також усі три перелічені, але є чітка різниця щодо того, який принцип абстракції ви можете використовувати для будь-яких речей. І ви можете стверджувати, що ця відмінність є досить умовною.

Шлях Скали

Ми вирішили мати однакові принципи побудови для всіх трьох типів членів .
Таким чином, ви можете мати абстрактні поля, а також значення параметрів.
Ви можете передавати методи (або "функції") як параметри, або ви можете абстрагуватися над ними.
Ви можете вказати типи як параметри, або ви можете абстрагуватися над ними.
І що ми отримуємо концептуально, це те, що ми можемо моделювати одне з точки зору іншого. Принаймні в принципі ми можемо виражати всі види параметризації як форму об'єктно-орієнтованої абстракції. Тож у певному сенсі можна сказати, що Скала є більш ортогональною і повною мовою.

Чому?

Що, зокрема, купує вас абстрактні типи, - це приємне лікування цих проблем коваріації, про які ми говорили раніше.
Одна зі стандартних проблем, яка існує вже давно, - це проблема тварин та продуктів харчування.
Головоломка повинна була мати клас Animalз методом eat, який їсть деяку їжу.
Проблема полягає в тому, що якщо ми підкласи «Тварини» і маємо такий клас, як Корова, то вони їдять тільки траву, а не довільну їжу. Наприклад, корова не могла їсти рибу.
Те, що ви хочете, - це вміти сказати, що Корова має метод їжі, який їсть лише траву, а не інші речі.
Насправді, ви не можете цього зробити на Java, оскільки, виявляється, ви можете створити недоброзичливі ситуації, як-от проблема присвоєння фруктової змінної Apple, про яку я говорив раніше.

Відповідь полягає в тому, що ви додаєте абстрактний тип до класу Animal .
Ви кажете, у мого нового класу Тварини є тип, про SuitableFoodякий я не знаю.
Так це абстрактний тип. Ви не даєте реалізації цього типу. Тоді у вас є eatметод, який їсть тільки SuitableFood.
І тоді в Cowкласі я б сказав: Добре, у мене є Корова, яка розширює клас Animal, і для Cow type SuitableFood equals Grass.
Тож абстрактні типи дають це поняття типу в суперкласі, який я не знаю, і який потім я заповнюю пізніше в підкласах тим, що я знаю .

Те саме з параметризацією?

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


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 у підкласах тими підтипами упорядкованих, для яких член Oтипу equals T.
Через це обмеження <метод класу Orders гарантовано застосовується до приймача та аргументу типу 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це "параметр кріплення".
Їм все одно потрібно було б розібратися, що означає "параметр кріплення", але вони могли принаймні отримати назву типу, не дивлячись у документацію.


61
Як я повинен отримувати бали карми, відповідаючи на запитання Скали, коли ти приїжджаєш і робиш це ??? :-)
Даніель К. Собрал

7
Привіт, Даніель: Я думаю, що повинні бути конкретні приклади для ілюстрації переваг абстрактних типів перед параметризацією. Опублікувати деякі теми в цій темі було б гарним початком;) Я знаю, що я б підтримав це.
VonC

1
Чи вважаєте ви, що наведене нижче є справедливим підсумком: абстрактні типи використовуються у відносинах "має-а" або "вживає-в" (наприклад, корова їсть траву), де як генерики зазвичай 'стосуються' відносини (наприклад, Список інтів)
thatismatt

Я не впевнений, що стосунки між абстрактними типами або загальними ознаками відрізняються. Відмінність полягає в тому, як вони використовуються та як керуються межі параметрів. Більше в моїй відповіді за мить.
VonC

1
Зауважте до себе: дивіться також цю публікацію в блозі травня 2010 року: daily-scala.blogspot.com/2010/05/…
VonC,

37

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

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

Якщо ви використовуєте абстрактний тип, то люди будуть змушені створювати підклас. Люди будуть потрібні класи , як AnyBuffer, StringBufferі т.д.

Вам потрібно вирішити, що краще для вашої конкретної потреби.


18
ммм тонких сильно покращився на цьому фронті, можна просто вимагати Buffer { type T <: String }або Buffer { type T = String }залежно від ваших потреб
Eduardo Pareja Tobes

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

Тепер ми можемо написати конкретні подання для описаного шаблону, визначити ліві та об’єднати методи у всіх класах, а отримати право та подвійне безкоштовно

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

Отже, і абстрактні типи, і параметри типу використовуються для створення абстракцій. У них обох є слабка і сильна сторона. Абстрактні типи більш конкретні і здатні описати будь-яку структуру типу, але є багатослівними та потребують чіткого визначення. Параметри типу можуть створювати купу типів миттєво, але створює додаткові занепокоєння щодо спадкування та меж типу.

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


0

Я думаю, що тут немає великої різниці. Тип абстрактних членів можна розглядати як просто екзистенційні типи, що схоже на типи записів у деяких інших функціональних мовах.

Наприклад, у нас є:

class ListT {
  type T
  ...
}

і

class List[T] {...}

Тоді ListTтак само, як і List[_]. Переконаність членів типу полягає в тому, що ми можемо використовувати клас без явного конкретного типу та уникати занадто багато параметрів типу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.