Tutaj masz dobry punkt widzenia na ten temat:
Cel systemu typów Scali
Rozmowa z Martinem Oderskim, część III
, Bill Venners i Frank Sommers (18 maja 2009)
Aktualizacja (październik 2009): to, co następuje poniżej, zostało zilustrowane w tym nowym artykule przez Billa Vennersa:
Członkowie typu abstrakcyjnego kontra parametry typu ogólnego w Scali (patrz podsumowanie na końcu)
(Oto odpowiedni fragment pierwszego wywiadu z maja 2009 r., Moje podkreślenie)
Ogólna zasada
Zawsze istniały dwa pojęcia abstrakcji:
- parametryzacja i
- członkowie abstrakcyjni.
W Javie masz również oba, ale zależy to od tego, nad czym abstrahujesz.
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 elementów typu abstrakcyjnego, ale możesz określić typ jako parametr.
Tak więc w Javie masz również wszystkie trzy, ale istnieje różnica w tym, jakiej zasady abstrakcji możesz użyć do jakich rzeczy. I można argumentować, że to rozróżnienie jest dość arbitralne.
Droga Scali
Postanowiliśmy mieć takie same zasady konstrukcyjne dla wszystkich trzech rodzajów członków .
Możesz mieć zarówno pola abstrakcyjne, jak i parametry wartości.
Możesz przekazać metody (lub „funkcje”) jako parametry lub możesz nad nimi nadpisać.
Możesz określić typy jako parametry lub możesz nad nimi streścić.
A koncepcyjnie otrzymujemy to, że możemy modelować jedno pod względem drugiego. Przynajmniej w zasadzie możemy wyrazić każdy rodzaj parametryzacji jako formę abstrakcji obiektowej. W pewnym sensie można powiedzieć, że Scala jest bardziej ortogonalnym i kompletnym językiem.
Czemu?
To, co w szczególności kupują ci typy abstrakcyjne, jest dobrym sposobem na rozwiązanie problemów z kowariancją, o których mówiliśmy wcześniej.
Jednym ze standardowych problemów, który istnieje od dłuższego czasu, jest problem zwierząt i żywności.
Łamigłówką było 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, wtedy zjadają tylko trawę, a nie arbitralne jedzenie. Na przykład krowa nie mogła zjeść ryby.
Chcecie móc powiedzieć, że Krowa ma metodę jedzenia, która zjada tylko Trawę, a nie inne rzeczy.
W rzeczywistości nie można tego zrobić w Javie, ponieważ okazuje się, że można konstruować niepożądane sytuacje, takie jak problem przypisywania Fruit do zmiennej Apple, o której mówiłem wcześniej.
Odpowiedź jest taka, że dodajesz abstrakcyjny typ do klasy Animal .
Mówisz, że moja nowa klasa Zwierząt ma pewien rodzaj SuitableFood
, którego nie znam.
To jest typ abstrakcyjny. Nie podajesz implementacji tego typu. Następnie masz eat
metodę, która je tylko SuitableFood
.
A potem w Cow
klasie powiedziałbym: OK, mam Krowę, która przedłuża klasę Animal
i na Cow type SuitableFood equals Grass
.
Tak więc typy abstrakcyjne zapewniają takie pojęcie typu w nadklasie, której nie znam, którą następnie wypełniam w podklasach czymś, co wiem .
To samo z parametryzacją?
Rzeczywiście możesz. Możesz sparametryzować klasę Zwierzę z rodzajem spożywanego jedzenia.
Ale w praktyce, gdy robisz to z wieloma różnymi rzeczami, prowadzi to do eksplozji parametrów , a co więcej, w granicach parametrów .
Na konferencji ECOOP w 1998 r. Kim Bruce, Phil Wadler i ja mieliśmy artykuł, w którym pokazaliśmy, że wraz ze wzrostem 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 wybuchu.
thatismatt pyta w komentarzach:
Czy uważasz, że następujące podsumowanie jest rzetelne:
- Typy abstrakcyjne są używane w relacjach „has-a” lub „uses-a” (np. A
Cow eats Grass
)
- gdzie jako generyczne są zwykle relacje „of” (np.
List of Ints
)
Nie jestem pewien, czy relacja różni się między używaniem typów abstrakcyjnych lub generycznych. Różnica polega na:
- jak są one używane, oraz
- jak zarządza się granicami parametrów.
Aby zrozumieć, o czym mówi Martin, jeśli chodzi o „eksplozję parametrów, a co więcej, w granicach parametrów ”, i jej późniejszy kwadratowy wzrost, gdy typ abstrakcyjny jest modelowany za pomocą ogólnych, możesz rozważyć artykuł „ Skalowalna abstrakcja komponentów ”napisane przez ... Martina Odersky'ego i Matthiasa Zengera dla OOPSLA 2005, o których mowa w publikacjach projektu Palcom (zakończonych w 2007 roku).
Odpowiednie wyciągi
Definicja
Elementy typu abstrakcyjnego zapewniają elastyczny sposób na streszczenie nad konkretnymi typami komponentów.
Typy abstrakcyjne mogą ukrywać informacje o elementach wewnętrznych komponentu, podobnie do ich użycia w podpisach SML . W zorientowanym obiektowo frameworku, w którym klasy mogą być rozszerzane przez dziedziczenie, można je również wykorzystywać jako elastyczny sposób parametryzacji (często nazywany polimorfizmem rodziny, patrz na przykład ten wpis w blogu i artykuł napisany przez Erica Ernsta ).
(Uwaga: polimorfizm rodziny został zaproponowany dla języków obiektowych jako rozwiązanie wspierające klasy rekursywne wielokrotnego użytku, ale bezpieczne dla typu.
Kluczową ideą polimorfizmu rodziny jest pojęcie rodzin, które są używane do grupowania klas rekurencyjnych)
abstrakcja typu ograniczona
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 górną granicą typu, która składa się z nazwy klasy Uporządkowana i uściślenia { type O = T }
.
Górną granicę ogranicza specjalności T u podklas do tych podtypów uporządkowanych w którym człon typu O
o equals T
.
Z powodu tego ograniczenia <
gwarantuje się , że metoda klasy Uporządkowana ma zastosowanie do odbiornika i argumentu typu T.
Przykład pokazuje, że ograniczony element typu może sam pojawić się jako część granicy.
(tj. Scala obsługuje polimorfizm związany z F )
(Uwaga: od Petera Canninga, Williama Cooka, Waltera Hilla, Waltera Olthoffa:
Ograniczona kwantyfikacja została wprowadzona przez Cardellego i Wegnera jako sposób pisania funkcji, które działają jednakowo na wszystkich podtypach danego typu.
Zdefiniowali prosty model „obiektowy” i zastosował ograniczoną kwantyfikację do funkcji sprawdzania typu, które mają sens dla wszystkich obiektów posiadających określony zestaw „atrybutów”.
Bardziej realistyczna prezentacja języków zorientowanych obiektowo pozwoliłaby obiektom, które są elementami typów definiowanych rekurencyjnie .
W tym kontekście ograniczona kwantyfikacja nie spełnia już zamierzonego celu. Łatwo jest znaleźć funkcje, które mają sens na wszystkich obiektach posiadających określony zestaw metod, ale których nie można wpisać w systemie Cardelli-Wegner.
Aby zapewnić podstawę dla typowanych funkcji polimorficznych w językach obiektowych, wprowadzamy kwantyfikację F)
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 funkcjonalnych, natomiast druga forma jest zwykle używana w językach obiektowych.
Tradycyjnie Java obsługuje parametryzację wartości i abstrakcję elementu dla operacji. Nowsza wersja Java 5.0 z generics obsługuje parametryzację również dla typów.
Argumenty przemawiające za włączeniem leków generycznych do Scali są dwojakie:
Po pierwsze, kodowanie do typów abstrakcyjnych nie jest tak łatwe do zrobienia ręcznie. Oprócz utraty zwięzłości istnieje również problem przypadkowych konfliktów nazw między abstrakcyjnymi nazwami typów, które emulują parametry typu.
Po drugie, rodzaje generyczne i typy abstrakcyjne zwykle pełnią różne role w programach Scala.
- Generyczne są zwykle używane, gdy trzeba tylko utworzyć wystąpienie typu , natomiast
- typy abstrakcyjne są zwykle używane, gdy trzeba odwoływać się do typu abstrakcyjnego z kodu klienta .
Ten ostatni powstaje w szczególności w dwóch sytuacjach:
- Można chcieć ukryć dokładną definicję elementu typu przed kodem klienta, aby uzyskać rodzaj enkapsulacji znanej z systemów modułów typu SML.
- Lub ktoś może chcieć zastąpić typ kowariantnie w podklasach, aby uzyskać polimorfizm rodziny.
W systemie z ograniczonym polimorfizmem przepisywanie typu abstrakcyjnego na rodzajowy może pociągać za sobą kwadratowe rozszerzenie granic typu .
Aktualizacja z października 2009 r
Członkowie typu abstrakcyjnego a ogólne parametry typu w Scali (Bill Venners)
(moje podkreślenie)
Dotychczasowe spostrzeżenia dotyczące elementów typu abstrakcyjnego są takie, że są one przede wszystkim lepszym wyborem niż ogólne parametry typu, gdy:
- chcesz pozwolić ludziom mieszać definicje tego typu za pomocą cech .
- myślisz, że wyraźna wzmianka o nazwie elementu typu podczas jej definiowania pomoże w czytelności kodu .
Przykład:
jeśli chcesz przekazać trzy różne obiekty urządzeń do testów, będziesz mógł to zrobić, ale musisz podać trzy typy, po jednym dla każdego parametru. Tak więc, gdybym przyjął podejście oparte na parametrach typu, Twoje klasy pakietów mogłyby wyglądać następująco:
// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
// ...
}
Natomiast przy podejściu typu członka będzie to wyglądać tak:
// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
// ...
}
Inną drobną różnicą między elementami typu abstrakcyjnego a parametrami typu ogólnego jest to, że gdy określony jest parametr typu ogólnego, czytniki kodu nie widzą nazwy parametru typu. W ten sposób ktoś zobaczył ten wiersz kodu:
// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
// ...
}
Nie wiedzieliby, jaka jest nazwa parametru typu określonego jako StringBuilder bez wyszukiwania. Podczas gdy nazwa parametru type znajduje się w kodzie w abstrakcyjnym podejściu członka typu:
// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
type FixtureParam = StringBuilder
// ...
}
W drugim przypadku czytelnicy kodu mogą zobaczyć, że StringBuilder
jest to typ „parametru urządzenia”.
Wciąż będą musieli dowiedzieć się, co oznacza „parametr urządzenia”, ale mogliby przynajmniej uzyskać nazwę typu bez zaglądania do dokumentacji.
Miałem to samo pytanie, kiedy czytałem o Scali.
Zaletą używania generycznych jest to, że tworzysz rodzinę typów. Nikt nie będzie musiał podklasy
Buffer
mogą -Oni prostu użyćBuffer[Any]
,Buffer[String]
itpJeśli użyjesz typu abstrakcyjnego, ludzie będą zmuszeni utworzyć podklasę. Ludzie będą musieli zajęcia jak
AnyBuffer
,StringBuffer
itpMusisz zdecydować, który jest lepszy dla twojej konkretnej potrzeby.
źródło
Buffer { type T <: String }
lub wBuffer { type T = String }
zależności od swoich potrzebMożesz używać typów abstrakcyjnych w połączeniu z parametrami typów, aby tworzyć niestandardowe szablony.
Załóżmy, że musisz ustanowić wzór z trzema połączonymi cechami:
w sposób, w jaki argumenty wymienione w parametrach typu są z szacunkiem AA, BB, CC
Możesz przyjść z jakimś kodem:
co nie działałoby w ten prosty sposób z powodu wiązań parametrów typu. Aby dziedziczyć poprawnie, musisz ustawić kowariant
Ta jedna próbka zostałaby skompilowana, ale stawia wysokie wymagania dotyczące zasad wariancji i nie można jej użyć w niektórych przypadkach
Kompilator będzie sprzeciwił się błędem sprawdzania wariancji
W takim przypadku możesz zebrać wszystkie wymagania typu dla dodatkowej cechy i sparametryzować inne cechy
Teraz możemy napisać konkretną reprezentację dla opisanego wzoru, zdefiniować metody lewej i połączyć we wszystkich klasach i uzyskać prawo i podwój za darmo
Tak więc do tworzenia abstrakcji używane są zarówno typy abstrakcyjne, jak i parametry typu. Obaj mają słabą i mocną stronę. Typy abstrakcyjne są bardziej szczegółowe i mogą opisywać dowolną strukturę typów, ale są pełne i wymagają wyraźnego określenia. Parametry typu mogą natychmiast tworzyć wiązkę typów, ale dodatkowo martwi Cię dziedziczeniem i ograniczeniami typów.
Zapewniają sobie synergię i mogą być używane łącznie do tworzenia złożonych abstrakcji, których nie da się wyrazić tylko jednym z nich.
źródło
Myślę, że nie ma tutaj dużej różnicy. Elementy abstrakcyjne typów mogą być postrzegane jako tylko typy egzystencjalne, które są podobne do typów rekordów w niektórych innych językach funkcjonalnych.
Na przykład mamy:
i
To
ListT
jest tak samo jakList[_]
. Konsekwencją elementów typu jest to, że możemy używać klasy bez wyraźnego konkretnego typu i unikać zbyt wielu parametrów typu.źródło