Różnica między interfejsami OOP a klasami typu FP [duplikat]

Możliwy duplikat:
Interfejs Javy i klasa typów Haskella: różnice i podobieństwa?

Kiedy zacząłem uczyć się Haskella, powiedziano mi, że typy są potężniejsze niż interfejsy.

Rok później intensywnie używałem interfejsów i typeklasów i jeszcze nie widziałem przykładu lub wyjaśnienia, jak się różnią. To albo nie jest objawienie, które przychodzi naturalnie, przegapiłem coś oczywistego, albo w rzeczywistości nie ma żadnej prawdziwej różnicy.

Przeszukanie Internetu nie wykazało niczego istotnego. Masz odpowiedź?

Author: Community, 2011-11-14

4 answers

Możesz spojrzeć na to pod wieloma kątami. Inni się nie zgodzą, ale myślę, że interfejsy OOP to dobre miejsce, aby zacząć od zrozumienia klas typu(na pewno w porównaniu do zaczynania od niczego).

Ludzie lubią zwracać uwagę, że koncepcyjnie klasy typów klasyfikują typy, podobnie jak zestawy - "zbiór typów, które wspierają te operacje, wraz z innymi oczekiwaniami, których nie można zakodować w samym języku". Ma to sens i od czasu do czasu robi się to, aby zadeklarować typuj klasę bez metod, mówiąc "uczyń swój typ instancją tej klasy tylko wtedy, gdy spełnia określone wymagania". Zdarza się to bardzo rzadko w przypadku interfejsów OOP.

Jeśli chodzi o konkretne różnice, istnieje wiele sposobów, w jaki klasy typów są potężniejsze niż interfejsy OOP:

  • Największym z nich jest to, że typeklasy oddzielają deklarację, że typ implementuje interfejs od deklaracji samego typu. Dzięki interfejsom OOP można wymienić interfejsy typ implementuje się, gdy go zdefiniujesz, i nie ma sposobu, aby dodać więcej później. W przypadku klas typu, jeśli tworzysz nową klasę typu, którą dany typ "up the module hierarchy" mógłby zaimplementować, ale o której nie wie, możesz napisać deklarację instancji. Jeśli masz typ i klasę typu od osobnych stron trzecich, które nie wiedzą o sobie nawzajem, możesz napisać deklarację instancji. W analogicznych przypadkach z interfejsami OOP, w większości po prostu utknąłeś, choć języki OOP ewoluowały " projektowanie patterns " (adapter), aby obejść ograniczenie.

  • Następnym największym (jest to oczywiście subiektywne) jest to, że chociaż koncepcyjnie, interfejsy OOP są zbiorem metod, które mogą być wywoływane na obiektach implementujących interfejs, klasy typu są zbiorem metod, które mogą być używane z typami, które są członkami klasy. Rozróżnienie jest ważne. Ponieważ metody klasy type są definiowane w odniesieniu do typu, A Nie obiektu, nie ma przeszkód, aby mieć metody z wieloma obiektami typu jako parametrami( operatory równości i porównania) lub które zwracają obiekt typu jako wynik( różne operacje arytmetyczne), a nawet stałe typu (minimalna i maksymalna granica). Interfejsy OOP po prostu nie mogą tego zrobić, a języki OOP ewoluowały wzorce projektowe (np. metoda virtual clone), aby obejść to ograniczenie.

  • Interfejsy OOP mogą być definiowane tylko dla typów; klasy typów mogą być również definiowane dla tak zwanych "konstruktory typu". Różne typy kolekcji zdefiniowane za pomocą szablonów i generyków w różnych językach OOP pochodnych C są konstruktorami typów: List przyjmuje typ T jako argument i konstruuje listę typów. Klasy typu pozwalają zadeklarować interfejsy dla konstruktorów typu: powiedzmy, operacja mapowania dla typów kolekcji, która wywołuje podaną funkcję na każdym elemencie kolekcji i zbiera wyniki w nowej kopii kolekcji - potencjalnie z innym typem elementu! Ponownie, nie można tego zrobić z interfejsami OOP.

  • Jeśli dany parametr wymaga zaimplementowania wielu interfejsów, z klasami typów jest trywialnie łatwo wymienić, które z nich powinny być członkami; z interfejsami OOP można określić tylko jeden interfejs jako typ danego wskaźnika lub odniesienia. Jeśli potrzebujesz go zaimplementować więcej, jedynymi opcjami są nieatrakcyjne takie jak pisanie jednego interfejsu w podpisie i rzucanie do innych lub dodawanie oddzielnych parametrów dla każdy interfejs i wymaga, aby wskazywały na ten sam obiekt. Nie możesz nawet rozwiązać go deklarując nowy, pusty interfejs, który dziedziczy po tych, których potrzebujesz, ponieważ Typ nie będzie automatycznie uważany za implementację nowego interfejsu tylko dlatego, że implementuje jego przodków. (Gdybyś mógł zadeklarować implementacje po fakcie, to nie byłby to taki problem, ale tak, tego też nie możesz zrobić.)

  • Rodzaj odwróconego przypadku tego powyżej, można wymagać że dwa parametry mają typy implementujące określony interfejs i , że są one tego samego typu. Z interfejsami OOP można określić tylko pierwszą część.

  • Deklaracje instancji dla klas typów są bardziej elastyczne. W przypadku interfejsów OOP można tylko powiedzieć "deklaruję Typ X i implementuje on interfejs Y", gdzie X i Y są specyficzne. W klasach typu można powiedzieć "wszystkie typy List, których typy elementów spełniają te warunki, są członkami Y". (Można również powiedzmy "wszystkie typy, które są członkami X i Y są również członkami Z", chociaż w Haskell jest to problematyczne z wielu powodów.)

  • Tak zwane "ograniczenia klasy nadrzędnej" są bardziej elastyczne niż zwykłe dziedziczenie interfejsu. W przypadku interfejsów OOP można tylko powiedzieć "aby Typ implementował ten interfejs, musi on również zaimplementować te inne Interfejsy". Jest to również najczęstszy przypadek z klasami typu, ale ograniczenia klasy superklasowej pozwalają również mówić takie rzeczy, jak "SomeTypeConstructor musi zaimplementować interfejs typu so-and-so", lub "Wyniki funkcji tego typu zastosowanej do typu muszą spełniać ograniczenie typu so-and-so", I tak dalej.

  • Jest to obecnie rozszerzenie języka w Haskell (podobnie jak funkcje typu), ale Można zadeklarować klasy typu obejmujące wiele typów. Na przykład klasa izomorfizmu: Klasa par typów, w której można bezstratnie konwertować z jednego do drugiego i z powrotem. Ponownie, nie jest to możliwe z OOP interfejsy.

  • Na pewno jest tego więcej.

Warto zauważyć, że w językach OOP, które dodają generyki, niektóre z tych ograniczeń można usunąć(czwarty, piąty, ewentualnie drugi punkt).

Z drugiej strony, są dwie ważne rzeczy, które mogą zrobić interfejsy OOP, a klas typu natywnie nie:

  • Dynamiczna wysyłka w trybie Runtime. W językach OOP banalne jest przekazywanie i przechowywanie wskaźników do obiektu realizującego interfejsu, oraz wywoływać na nim metody w trybie runtime, które zostaną rozwiązane zgodnie z typem Dynamic, runtime obiektu. Dla kontrastu, ograniczenia klas typów są domyślnie określane w czasie kompilacji - i być może zaskakujące, w większości przypadków jest to wszystko, czego potrzebujesz. Jeśli potrzebujesz dynamicznej wysyłki, możesz użyć tak zwanych typów egzystencjalnych( które są obecnie rozszerzeniem języka w Haskell): konstrukcji, w której "zapomina", jaki był typ obiektu i tylko zapamiętuje (według własnego wyboru), że przestrzega pewnych ograniczeń klas typów. Od tego momentu zachowuje się w zasadzie dokładnie tak samo jak wskaźniki lub odniesienia do obiektów implementujących interfejsy w językach OOP, A klasy typów nie mają deficytu w tym obszarze. (Należy zauważyć, że jeśli masz dwa existentials implementujące tę samą klasę type I metodę klasy type, która wymaga dwóch parametrów swojego typu, nie możesz użyć existentials jako parametrów, ponieważ nie możesz wiedzieć, czy albo nie egzystencjały miały ten sam typ. Ale w porównaniu z językami OOP, które nie mogą mieć takich metod w pierwszej kolejności, nie jest to strata.)

  • Runtime casting obiektów do interfejsów. W językach OOP, można wziąć wskaźnik lub odniesienie w czasie wykonywania i sprawdzić, czy implementuje interfejs, i "cast" go do tego interfejsu, jeśli tak. Klasy typu nie mają natywnie niczego równoważnego (co jest pod pewnymi względami zaletą, ponieważ zachowuje właściwość o nazwie "parametryzacja", ale nie będę się w to tutaj wdawał). Oczywiście nic nie stoi na przeszkodzie dodaniu nowej klasy typu (lub powiększeniu istniejącej) za pomocą metod do tworzenia obiektów tego typu do istniejących klas typu, które chcesz. (Możesz również zaimplementować taką możliwość bardziej ogólnie jako bibliotekę, ale jest ona znacznie bardziej zaangażowana. Planuję go skończyć i wgrać do Hackage pewnego dnia , obiecuję!)

(powinienem zaznaczyć, że podczas gdy możesz * zrobić te rzeczy, Wiele osób uważa emulowanie OOP w ten sposób zły styl i sugeruje użycie bardziej prostych rozwiązań, takich jak jawne zapisy funkcji zamiast klas typu. Dzięki pełnym funkcjom pierwszej klasy opcja ta jest nie mniej wydajna.)

Operacyjnie interfejsy OOP są zwykle implementowane przez przechowywanie wskaźnika lub wskaźników w samym obiekcie, które wskazują tabele wskaźników funkcji dla interfejsów, które obiekt implementuje. Klasy typu są zazwyczaj implementowane (dla języki, które wykonują polimorfizm po boksie, jak Haskell, a nie polimorfizm po multiinstrukcji, jak C++) poprzez "przekazywanie słownikowe": kompilator domyślnie przekazuje wskaźnik do tabeli funkcji (i stałych) jako ukryty parametr do każdej funkcji, która używa klasy type, a funkcja dostaje jedną kopię bez względu na to, ile obiektów jest zaangażowanych (dlatego możesz robić rzeczy wymienione w drugim punkcie powyżej). Implementacja typów egzystencjalnych wygląda jak co robią języki OOP: wskaźnik do klasy type dictionary jest przechowywany wraz z obiektem jako "dowód", że typ "zapomniany" jest jego członkiem.

Jeśli kiedykolwiek czytałeś o propozycji 'concepts' dla C++ (tak jak pierwotnie zaproponowano dla C++11), jest to w zasadzie klasy typów Haskella przerobione dla szablonów C++. Czasami myślę, że byłoby miło mieć język, który po prostu zabiera C++z pojęciami, zrywa z funkcji obiektowych i wirtualnych połowę, czyści w górę składni i innych brodawek i dodaje typy egzystencjalne, gdy potrzebujesz dynamicznej wysyłki opartej na typach uruchomieniowych. (Update: Rust {[70] } jest w zasadzie tym, z wieloma innymi fajnymi rzeczami.)

 115
Author: glaebhoerl,
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-10-28 19:18:03

Zakładam, że mówisz o klasach typu Haskell. To nie jest tak naprawdę różnica między interfejsami i klasami typów. Jak sama nazwa wskazuje, Klasa type jest po prostu klasą typów ze wspólnym zestawem funkcji (i powiązanych typów, jeśli włączysz rozszerzenie TypeFamilies).

Jednak system typów Haskella jest sam w sobie potężniejszy niż, na przykład, system typów C#. Pozwala to na pisanie klas typu w Haskell, których nie można wyrazić w C#. Nawet Klasa typu jak proste jak Functor nie może być wyrażone w C#:
class Functor f where
    fmap :: (a -> b) -> f a -> f b
Problem z C# polega na tym, że generyki same w sobie nie mogą być generyczne. Innymi słowy, w C# tylko typy rodzaju * mogą być polimorficzne. Haskell umożliwia konstruktory typu polimorficznego, więc typy dowolnego rodzaju mogą być polimorficzne. To jest powód, dla którego wiele potężnych funkcji generycznych w Haskell (mapM, liftA2, itd.) nie może być wyrażona w większości języków z mniej wydajnym systemem typów.
 15
Author: ertes,
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
2011-11-14 13:43:31
 5
Author: nponeccop,
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
2011-11-14 14:46:50

Główna różnica-która sprawia, że klasy typu są znacznie bardziej elastyczne niż interfejsy-polega na tym, że klasy typu są niezależne od swoich typów danych i mogą być dodawane później. Inną różnicą (przynajmniej w stosunku do Javy) jest to, że można podać domyślne implementacje. Przykład:

//Java
public interface HasSize {
   public int size();
   public boolean isEmpty();
}

Posiadanie tego interfejsu jest miłe, ale nie ma sposobu, aby dodać go do istniejącej klasy bez jej zmiany. Jeśli masz szczęście, klasa nie jest ostateczna (powiedzmy ArrayList), więc możesz napisać podklasę implementacja interfejsu do niego. Jeśli klasa jest ostateczna (powiedzmy String), masz pecha.

Porównaj to z Haskellem. Możesz napisać klasę typu:
--Haskell
class HasSize a where
  size :: a -> Int
  isEmpty :: a -> Bool
  isEmpty x = size x == 0

I możesz dodawać istniejące typy danych do klasy bez dotykania ich:

instance HasSize [a] where
   size = length

Kolejną ładną właściwością klas typu jest wywołanie niejawne. Na przykład, jeśli masz Comparator w Javie, musisz przekazać ją jako wartość jawną. W Haskell odpowiednik Ord może być użyty automatycznie, gdy tylko odpowiedni instancja jest w zasięgu.

 4
Author: Landei,
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
2013-10-22 08:24:42