Scala: typy abstrakcyjne a generyki

Czytałam wycieczka po Scali: abstrakcyjne typy. Kiedy lepiej używać typów abstrakcyjnych?

Na przykład,

abstract class Buffer {
  type T
  val element: T
}

Raczej, że generyki, na przykład,

abstract class Buffer[T] {
  val element: T
}
Author: Peter Mortensen, 2009-07-20

3 answers

Masz dobry punkt widzenia na ten temat tutaj:

Przeznaczenie systemu typu Scala
A Conversation with Martin Odersky, Part III
w 2009 roku, w 2010 roku, w Polsce i na świecie, w 2011 roku, w 2012 roku, w 2013 roku, w Polsce i na świecie.]}

Aktualizacja (październik 2009): to, co poniżej zostało zilustrowane w tym nowym artykule Billa Vennersa:
elementy typu abstrakcyjnego kontra parametry typu ogólnego w Scali (patrz podsumowanie na końcu)


(Oto odpowiedni fragment pierwszego wywiadu, maj 2009, kursywa moja)

Ogólna zasada

Zawsze istniały dwa pojęcia abstrakcji:

  • parametryzacja i
  • członkowie abstrakcyjni.

W Javie masz również oba, ale to zależy od tego, nad czym się abstrakcjonizujesz.
W Javie masz metody abstrakcyjne, ale nie możesz przekazać metody jako parametru.
Nie masz pól abstrakcyjnych, ale możesz przekazać wartość jako parametr.
Podobnie nie masz członków typu abstrakcyjnego, ale możesz określić typ jako parametr.
Więc w Javie masz również wszystkie trzy z nich, ale jest różnica co do zasady abstrakcji, której możesz użyć do jakich rzeczy. I można argumentować, że to rozróżnienie jest dość arbitralne.

The Scala Way

Postanowiliśmy mieć te same zasady konstrukcyjne dla wszystkich trzech rodzajów członków .
Więc możesz mieć pola abstrakcyjne jak również parametry wartości.
Możesz przekazać metody (lub "funkcje") jako parametry, lub możesz nad nimi abstrakcyjnie.
Typy można określać jako parametry lub abstrakcyjnie nad nimi.
A to, co otrzymujemy koncepcyjnie, to to, że możemy modelować jedno w kategoriach drugiego. Przynajmniej w zasadzie możemy wyrazić każdy rodzaj parametryzacji jako formę abstrakcji zorientowanej obiektowo. Więc w pewnym sensie można powiedzieć, że Scala jest bardziej ortogonalna i kompletna język.

Dlaczego?

To, co, w szczególności, typy abstrakcyjne kupują ci, jest miłym leczeniem tych problemów kowariancji , o których rozmawialiśmy wcześniej.
Standardowym problemem, który istnieje od dawna, jest problem zwierząt i żywności.
Układanka miała mieć klasę Animal z metodą, eat, która zjada trochę jedzenia.
Problem polega na tym, że jeśli podklasujemy zwierzęta i mamy klasę taką jak krowa, to jedzą tylko trawę, a nie dowolne jedzenie. Krowa na przykład nie mogłem zjeść ryby.
To, czego chcesz, to móc powiedzieć, że krowa ma metodę jedzenia, która je tylko trawę, a nie inne rzeczy.
Właściwie nie można tego zrobić w Javie, ponieważ okazuje się, że można konstruować sytuacje niesolidne, takie jak problem przypisania owocu do zmiennej Apple, o którym mówiłem wcześniej.

Odpowiedź jest taka, że dodajesz abstrakcyjny typ do klasy zwierząt.
Mówisz, że moja nowa klasa zwierząt ma typ SuitableFood, którego nie wiedzieć.
Więc jest to typ abstrakcyjny. Nie dajesz implementacji tego typu. Następnie masz metodę eat, która zjada tylko SuitableFood.
A potem w klasie Cow powiedziałbym: OK, mam krowę, która rozszerza klasę Animal, A Dla Cow type SuitableFood equals Grass.
Tak więc typy abstrakcyjne dostarczają tego pojęcia typu w superklasie, której nie znam, które później uzupełniam w podklasach czymś, co znam.

To samo z parametryzacją?

W rzeczy samej, możesz. Mógłbyś parametryzuj zwierzę klasy z rodzajem pożywienia, które je.
Ale w praktyce, kiedy robisz to z wieloma różnymi rzeczami, prowadzi to do eksplozji parametrów, A zazwyczaj, co więcej, w granicach parametrów.
Na ECOOP 1998, Kim Bruce, Phil Wadler i ja mieliśmy pracę, w której pokazaliśmy, że w miarę zwiększania liczby rzeczy, których nie wiesz, typowy program będzie rósł kwadratowo.
Są więc bardzo dobre powody, aby nie robić parametrów, ale mieć tych abstrakcyjnych członków, ponieważ nie dają ci tego kwadratowego wysadzenia.

Thatismatt pyta w komentarzach:

Czy uważasz, że poniższe podsumowanie jest sprawiedliwe:

    Typy abstrakcyjne są używane w relacjach "has-a" lub "uses-a" (np. a Cow eats Grass)
  • gdzie jako generyki są zwykle "związkami" (np. List of Ints)

Nie jestem pewien, czy związek jest taki, że różni się między używaniem typy abstrakcyjne lub generyczne. Czym się różni:

  • jak są one używane, i
  • jak zarządzane są granice parametrów.

Aby zrozumieć, o czym mówi Martin, jeśli chodzi o " eksplozję parametrów, a zwykle, co więcej, w granicach parametrów ", a następnie jej kwadratowy wzrost, gdy typ abstrakcyjny jest modelowany za pomocą generyków, można rozważyć artykuł "Scalable Component Abstraction" napisane by... Martin Odersky i Matthias Zenger dla OOPSLA 2005, wymienione w publikacje projektu Palcom (zakończony w 2007).

Odpowiednie wyciągi

Definicja

Pręty typu abstrakcyjnego zapewniają elastyczny sposób abstrakcji nad konkretnymi typami elementów.
Typy abstrakcyjne mogą ukrywać informacje o wewnętrznych elementach składowych, podobnie jak ich zastosowanie w SML podpisy. W zorientowanym obiektowo framework, w którym klasy mogą być rozszerzane przez dziedziczenie, może być również używany jako elastyczny sposób parametryzacji (często nazywany polimorfizmem rodziny, zobacz ten wpis weblog na przykład, I artykuł napisany przez Eric Ernst).

(Uwaga: polimorfizm rodziny został zaproponowany dla języków obiektowych jako rozwiązanie wspierające wielokrotnego użytku, ale bezpieczne dla typów wzajemnie rekurencyjne klasy.
Kluczową ideą polimorfizmu rodzinnego jest pojęcie rodzin, które są używane do grupowania wzajemnie rekurencyjnych klas)

Abstrakcja typu ograniczonego

abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}

Tutaj, deklaracja typu T jest ograniczona przez górną granicę typu, która składa się z uporządkowanej nazwy klasy i udoskonalenia { type O = T }.
Górna granica ogranicza specjalizacje T w podklasach do tych podtypów uporządkowanych, dla których członek typu O z equals T.
Ze względu na to ograniczenie, metoda < klasy uporządkowanej jest gwarantowana aby mieć zastosowanie do odbiornika i argumentu typu T.
Przykład pokazuje, że element typu ograniczonego może pojawić się jako część elementu ograniczonego.
Scala wspiera f-Ograniczony polimorfizm )

(notatka, od Peter Canning, William Cook, Walter Hill, Walter Olthoff papieru:
Kwantyfikacja limitowana została wprowadzona przez Cardellego i Wegnera jako sposób typowania funkcji, które działają jednolicie na wszystkich podtypach danego typu.
Zdefiniowali prosty model "obiektowy" i używał ograniczonego kwantyfikatora do wpisywania funkcji sprawdzających, które mają sens we wszystkich obiektach posiadających określony zestaw "atrybutów".
Bardziej realistyczna prezentacja języków obiektowych pozwoliłaby obiektom, które są elementami typów definiowanych rekurencyjnie .
W tym kontekście ograniczona kwantyfikacja nie służy już celowi zamierzonemu. Łatwo jest znaleźć funkcje, które mają sens na wszystkich obiektach posiadających określony zestaw metod, ale które nie mogą być wpisany w systemie Cardelli-Wegner.
Aby zapewnić podstawę dla typowanych funkcji polimorficznych w językach zorientowanych obiektowo, Wprowadzamy f-ograniczone kwantyfikację)

Dwie twarze tych samych monet]}

Istnieją dwie główne formy abstrakcji w językach programowania:

  • parametryzacja i
  • członkowie abstrakcyjni.

Pierwsza forma jest typowa dla języków funkcyjnych, podczas gdy druga jest zwykle używana w obiektowych języki.

Tradycyjnie, Java obsługuje parametryzację wartości i abstrakcję elementów dla operacji. Nowsza Java 5.0 z generics obsługuje parametryzację również dla typów.

Argumenty za włączeniem generyków do Scali są dwojakie:

  • Po pierwsze, kodowanie na typy abstrakcyjne nie jest tak proste do wykonania ręcznie. Oprócz utraty zwięzłości istnieje również problem przypadkowej nazwy konflikty między typami abstrakcyjnymi nazwy, które emulują parametry typu.

  • Po drugie, typy generyczne i abstrakcyjne zwykle pełnią różne role w programach Scali.
    • Generyki są zwykle używane, gdy potrzeba tylko instancjacji typu , podczas gdy
    • typy abstrakcyjne są zwykle używane, gdy trzeba odnieść się do abstraktu Wpisz z kodu klienta .
      Ten ostatni pojawia się w szczególności w dwóch sytuacjach:
    • ktoś może chcieć się ukryć dokładna definicja pręta typu z kodu klienta, aby uzyskać rodzaj enkapsulacji znany z systemów modułów w stylu SML.
    • można też nadpisać Typ kowariantnie w podklasach, aby uzyskać polimorfizm rodziny.
W systemie z ograniczonym polimorfizmem, przepisanie typu abstrakcyjnego na rodzajniki może pociągnąć za sobą kwadratową ekspansję granic typu .

Aktualizacja Październik 2009

Abstract Type Members versus Parametry typu ogólnego w Scali (Bill Venners)

(moje)

Moja dotychczasowa obserwacja o elementach typu abstrakcyjnego jest taka, że są one przede wszystkim lepszym Wyborem niż parametry typu ogólnego, gdy:

  • chcesz, aby ludzie mieszali definicje tych typów poprzez cechy.
  • myślisz, że wyraźne wskazanie nazwy członka typu podczas jego definiowania pomoże w czytelności kodu .

Przykład:

Jeśli chcesz przekazać do testów trzy różne obiekty osprzętu, będziesz w stanie to zrobić, ale musisz podać trzy typy, po jednym dla każdego parametru. Tak więc, gdybym przyjął podejście parametru typu, Twoje klasy suite mogłyby skończyć tak: {]}

// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
  // ...
}

Natomiast przy podejściu typu member będzie to wyglądało tak:

// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
  // ...
}

Jeszcze jedna drobna różnica między typem abstrakcyjnym prętów i parametrów typu generycznego jest to, że Gdy parametr typu generycznego jest określony, czytniki kodu nie widzą nazwy parametru typu. Tak więc ktoś widział tę linijkę kodu: {]}

// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
  // ...
}

Nie dowiedzieliby się, jaka jest nazwa parametru typu określonego jako StringBuilder, nie szukając go. Natomiast nazwa parametru type znajduje się w kodzie w abstrakcyjnym elemencie typu:

// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
  type FixtureParam = StringBuilder
  // ...
}

W tym ostatnim przypadek, Czytelnicy kodu mogli zobaczyć, że StringBuilder jest typem "parametr fixture".
Nadal musieliby dowiedzieć się, co oznacza "parametr urządzenia", ale mogliby przynajmniej uzyskać nazwę typu bez zaglądania do dokumentacji.

 229
Author: VonC,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:33:26

Miałem to samo pytanie, kiedy czytałem o Scali.

Zaletą używania generyków jest to, że tworzysz rodzinę typów. Nikt nie będzie musiał podklasować Buffer - mogą po prostu użyć Buffer[Any], Buffer[String], itd.

Jeśli użyjesz typu abstrakcyjnego, ludzie będą zmuszeni utworzyć podklasę. Ludzie będą potrzebowali takich zajęć jak AnyBuffer, StringBuffer, itd.

Musisz zdecydować, który jest lepszy dla twojej konkretnej potrzeby.
 36
Author: Daniel Yankowsky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-20 13:08:58

Możesz używać typów abstrakcyjnych w połączeniu z parametrami typu do tworzenia własnych szablonów.

Załóżmy, że musisz ustalić wzór z trzema połączonymi cechami:

trait AA[B,C]
trait BB[C,A]
trait CC[A,B]

W taki sposób, że argumenty wymienione w parametrach typu to AA, BB, CC same w sobie

Możesz przyjść z jakimś kodem:

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]]

Które nie działałyby w ten prosty sposób z powodu wiązań parametrów typu. Aby odziedziczyć poprawnie

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]]

Ta jedna próbka skompilowałaby się, ale ustanawia silne wymagania dotyczące zasad wariancji i nie może być używana w niektórych przypadkach

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
}

Kompilator będzie obiektem z mnóstwem błędów sprawdzania wariancji

W takim przypadku możesz zebrać wszystkie wymagania typu w dodatkowej cechy i parametryzować inne cechy

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

Teraz możemy napisać konkretną reprezentację dla opisanego wzorca, zdefiniować metody left I join we wszystkich klasach i uzyskać prawa i podwójne za darmo

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

Tak więc zarówno typy abstrakcyjne, jak i parametry typu są używane do tworzenia abstrakcji. Obaj mają słaby i silny punkt. Typy abstrakcyjne są bardziej specyficzne i zdolne do opisania dowolnej struktury typu, ale są wyraziste i wymagają wyraźnego sprecyzowania. Parametry typu mogą natychmiast utworzyć kilka typów, ale dodatkowo martwią Cię dziedziczenie i ograniczenia typów.

Dają sobie wzajemnie synergię i mogą być używane w połączeniu do tworzenia złożonych abstrakcji nie można tego wyrazić tylko jednym z nich.

 17
Author: ayvango,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-05 05:21:51