Я читав екскурсію по Скалі: абстрактні типи . Коли краще використовувати абстрактні типи?
Наприклад,
abstract class Buffer {
type T
val element: T
}
скоріше, щоб дженерики, наприклад,
abstract class Buffer[T] {
val element: T
}
Я читав екскурсію по Скалі: абстрактні типи . Коли краще використовувати абстрактні типи?
Наприклад,
abstract class Buffer {
type T
val element: T
}
скоріше, щоб дженерики, наприклад,
abstract class Buffer[T] {
val element: T
}
Відповіді:
Ви маєте хорошу точку зору на це питання тут:
Мета
розмови системи 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.
У системі з обмеженим поліморфізмом переписування абстрактного типу в генерику може спричинити за собою квадратичне розширення меж типу .
Члени абстрактного типу проти параметрів загального типу у 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
це "параметр кріплення".
Їм все одно потрібно було б розібратися, що означає "параметр кріплення", але вони могли принаймні отримати назву типу, не дивлячись у документацію.
У мене було те саме питання, коли я читав про Скалу.
Перевага використання дженерики полягає в тому, що ви створюєте сімейство типів. Ніхто не потрібно буде підклас Buffer
-вони можуть просто використовувати Buffer[Any]
, Buffer[String]
і т.д.
Якщо ви використовуєте абстрактний тип, то люди будуть змушені створювати підклас. Люди будуть потрібні класи , як AnyBuffer
, StringBuffer
і т.д.
Вам потрібно вирішити, що краще для вашої конкретної потреби.
Buffer { type T <: String }
або Buffer { type T = String }
залежно від ваших потреб
Ви можете використовувати абстрактні типи спільно з параметрами типу для встановлення спеціальних шаблонів.
Припустимо, вам потрібно встановити шаблон з трьома пов'язаними ознаками:
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)
}
Отже, і абстрактні типи, і параметри типу використовуються для створення абстракцій. У них обох є слабка і сильна сторона. Абстрактні типи більш конкретні і здатні описати будь-яку структуру типу, але є багатослівними та потребують чіткого визначення. Параметри типу можуть створювати купу типів миттєво, але створює додаткові занепокоєння щодо спадкування та меж типу.
Вони надають синергію один одному і можуть використовуватися спільно для створення складних абстракцій, які неможливо виразити лише однією з них.
Я думаю, що тут немає великої різниці. Тип абстрактних членів можна розглядати як просто екзистенційні типи, що схоже на типи записів у деяких інших функціональних мовах.
Наприклад, у нас є:
class ListT {
type T
...
}
і
class List[T] {...}
Тоді ListT
так само, як і List[_]
. Переконаність членів типу полягає в тому, що ми можемо використовувати клас без явного конкретного типу та уникати занадто багато параметрів типу.