Nie sądzę, że rozumiem klasy typów. Czytałem gdzieś, że myślenie o klasach typów jako „interfejsach” (od OO), które implementuje typ, jest błędne i wprowadza w błąd. Problem polega na tym, że mam problem z postrzeganiem ich jako czegoś innego i jak to jest złe.
Na przykład, jeśli mam klasę typu (w składni Haskell)
class Functor f where
fmap :: (a -> b) -> f a -> f b
Czym różni się to od interfejsu [1] (w składni Java)
interface Functor<A> {
<B> Functor<B> fmap(Function<B, A> fn)
}
interface Function<Return, Argument> {
Return apply(Argument arg);
}
Jedną z możliwych różnic, o których mogę pomyśleć, jest to, że implementacja klasy typu zastosowana przy pewnym wywołaniu nie jest określona, ale raczej określona na podstawie środowiska - powiedzmy, sprawdzając dostępne moduły dla implementacji dla tego typu. Wydaje się, że jest to artefakt implementacyjny, który można rozwiązać w języku OO; tak jak kompilator (lub środowisko wykonawcze) może skanować w poszukiwaniu wrappera / przedłużacza / monkey-patchera, który udostępnia wymagany interfejs dla danego typu.
czego mi brakuje?
[1] Uwaga: f a
argument został usunięty, fmap
ponieważ ponieważ jest to język OO, wywołałbyś tę metodę na obiekcie. Ten interfejs zakłada, że f a
argument został naprawiony.
C
bez obecności downcastów?Oprócz doskonałej odpowiedzi Andreasa, należy pamiętać, że klasy typów mają na celu usprawnienie przeciążenia , które wpływa na globalną przestrzeń nazw. W Haskell nie ma przeciążenia poza tym, co można uzyskać za pomocą klas typów. W przeciwieństwie do tego, gdy używasz interfejsów obiektowych, tylko funkcje zadeklarowane jako argumenty tego interfejsu będą musiały martwić się o nazwy funkcji w tym interfejsie. Interfejsy zapewniają więc lokalne przestrzenie nazw.
Na przykład
fmap
w interfejsie obiektowym o nazwie „Functor”. Byłoby całkowicie w porządku mieć innyfmap
w innym interfejsie, np. „Structor”. Każdy obiekt (lub klasa) może wybrać interfejs, który chce zaimplementować. Natomiast w Haskell możesz mieć tylko jednąfmap
w określonym kontekście. Nie można importować zarówno klas typu Functor, jak i Structor do tego samego kontekstu.Interfejsy obiektowe są bardziej podobne do standardowych sygnatur ML niż do klas typów.
źródło
W konkretnym przykładzie (z klasą typu Functor) implementacje Haskell i Java zachowują się inaczej. Wyobraź sobie, że masz typ danych Może i chcesz, aby był Functor (jest to bardzo popularny typ danych w Haskell, który możesz łatwo zaimplementować również w Javie). W swoim przykładzie Java sprawisz, że klasa może zaimplementować interfejs Functor. Więc możesz napisać następujące (tylko pseudo kod, ponieważ mam tylko tło c #):
Zauważ, że
res
ma typ Functor, a nie może. To sprawia, że implementacja Java jest prawie bezużyteczna, ponieważ tracisz informacje o konkretnym typie i musisz wykonywać rzutowania. (przynajmniej nie napisałem takiej implementacji, w której typy były nadal obecne). Z klasami typu Haskell otrzymasz w rezultacie Może Int.źródło
Maybe<Int>
.