Interfejs Java i klasa typów Haskella: różnice i podobieństwa?


112

Ucząc się Haskella, zwróciłem uwagę na jego klasę typów , która ma być wielkim wynalazkiem wywodzącym się od Haskella.

Jednak na stronie Wikipedii o klasie typu :

Programista definiuje klasę typów, określając zestaw nazw funkcji lub stałych wraz z odpowiadającymi im typami, które muszą istnieć dla każdego typu należącego do tej klasy.

Co wydaje mi się dość bliskie interfejsowi Java (cytując stronę Interfejs Wikipedii (Java) ):

Interfejs w języku programowania Java jest typem abstrakcyjnym używanym do określenia interfejsu (w ogólnym znaczeniu tego terminu), który muszą implementować klasy.

Te dwie rzeczy wyglądają raczej podobnie: klasa typu ogranicza zachowanie typu, podczas gdy interfejs ogranicza zachowanie klasy.

Zastanawiam się, jakie są różnice i podobieństwa między klasą typów w Haskellu a interfejsem w Javie, a może są one zasadniczo różne?

EDYCJA: Zauważyłem, że nawet haskell.org przyznaje, że są podobne . Jeśli są tak podobne (a może są?), To dlaczego klasa typu jest traktowana takim szumem?

WIĘCEJ EDYCJI: Wow, tyle świetnych odpowiedzi! Myślę, że będę musiał pozwolić społeczności zdecydować, która z nich jest najlepsza. Jednak czytając odpowiedzi, wszyscy wydają się po prostu mówić, że „istnieje wiele rzeczy, które typeklasa może zrobić, podczas gdy interfejs nie może lub musi radzić sobie z typami ogólnymi” . Nie mogę pomóc, ale zastanawiam się, czy jest coś , co interfejsy mogą zrobić, a typeklasy nie? Zauważyłem również, że Wikipedia twierdzi, że typeklasa została pierwotnie wynaleziona w artykule z 1989 r. * „Jak sprawić, by polimorfizm ad-hoc był mniej ad hoc”, podczas gdy Haskell wciąż jest w swojej kolebce, podczas gdy projekt Java został uruchomiony w 1991 roku i po raz pierwszy wydany w 1995 roku Więc może zamiast typeklasy podobnej do interfejsów, jest odwrotnie, że na interfejsy miała wpływ typeklasa?Czy są jakieś dokumenty / dokumenty potwierdzające lub obalające to? Dzięki za wszystkie odpowiedzi, wszystkie są bardzo pouczające!

Dzięki za wszystkie uwagi!


3
Nie, tak naprawdę nie ma nic, co interfejsy mogą zrobić, czego nie potrafią klasy typów, z głównym zastrzeżeniem, że interfejsy zwykle pojawiają się w językach, które mają wbudowane funkcje, których nie ma w Haskell. Gdyby klasy typów zostały dodane do języka Java, mogłyby również korzystać z tych funkcji.
CA McCann

8
Jeśli masz wiele pytań, zadaj kilka pytań, a nie próbuj wcisnąć je wszystkie w jedno pytanie. W każdym razie, odpowiadając na twoje ostatnie pytanie: głównym wpływem Javy jest Objective-C (a nie C ++, jak często jest fałszywie podawany), którego głównym wpływem z kolei są Smalltalk i C. Interfejsy Javy są adaptacją protokołów Objective-C, które są ponownie sformalizowanie idei protokołu w OO, która z kolei opiera się na idei protokołów w sieci, a konkretnie ARPANet. To wszystko wydarzyło się na długo przed cytowanym artykułem. ...
Jörg W Mittag

1
... Wpływ Haskella na Javę przyszedł znacznie później i ogranicza się do Generics, które w końcu zostały wspólnie zaprojektowane przez jednego z projektantów Haskella, Phila Wadlera.
Jörg W Mittag,

5
To jest artykuł Usenetu autorstwa Patricka Naughtona, jednego z oryginalnych projektantów Java: Na Javę silnie wpłynął Objective-C, a nie C ++ . Niestety, jest tak stary, że oryginalny wpis nie pojawia się nawet w archiwach Google.
Jörg W Mittag,

8
Jest jeszcze jedno pytanie, które zostało zamknięte jako dokładny duplikat tego, ale ma znacznie bardziej szczegółową odpowiedź: stackoverflow.com/questions/8122109/ ...
Ben

Odpowiedzi:


50

Powiedziałbym, że interfejs jest trochę podobny do klasy typu, w SomeInterface tktórej wszystkie wartości mają typ t -> whatever(gdzie whatevernie zawiera t). Dzieje się tak, ponieważ w przypadku rodzaju relacji dziedziczenia w Javie i podobnych językach wywoływana metoda zależy od typu obiektu, do którego są wywoływane, i od niczego innego.

Oznacza to, że naprawdę trudno jest stworzyć takie rzeczy jak add :: t -> t -> tw przypadku interfejsu, w którym jest on polimorficzny dla więcej niż jednego parametru, ponieważ nie ma możliwości, aby interfejs określił, że typ argumentu i typ zwracany metody są tego samego typu co typ obiekt, do którego jest wywoływany (tj. typ „self”). W przypadku Generics istnieją sposoby na sfałszowanie tego, tworząc interfejs z parametrem ogólnym, który powinien być tego samego typu co sam obiekt, na przykład jak Comparable<T>to robi, gdzie oczekuje się użycia, Foo implements Comparable<Foo>tak aby compareTo(T otherobject)rodzaj miał typ t -> t -> Ordering. Ale to nadal wymaga od programisty przestrzegania tej zasady, a także powoduje bóle głowy, gdy ludzie chcą stworzyć funkcję korzystającą z tego interfejsu, muszą mieć rekurencyjne parametry typu ogólnego.

Ponadto nie będziesz mieć takich rzeczy, jak empty :: tto, że nie wywołujesz tutaj funkcji, więc nie jest to metoda.


1
Cechy Scala (w zasadzie interfejsy) pozwalają na this.type, więc możesz zwracać lub akceptować parametry „typu własnego”. Scala ma zupełnie odrębną funkcję, którą nazywają „typami siebie”, która nie ma z tym nic wspólnego. Nic z tego nie jest różnicą koncepcyjną, tylko różnicą we wdrożeniach.
glina

45

Podobieństwo między interfejsami i klasami typów polega na tym, że nazywają i opisują zestaw powiązanych operacji. Same operacje są opisane za pomocą ich nazw, wejść i wyjść. Podobnie może istnieć wiele implementacji tych operacji, które prawdopodobnie będą się różniły.

Mając to na uwadze, oto kilka istotnych różnic:

  • Metody interfejsów są zawsze powiązane z instancją obiektu. Innymi słowy, zawsze istnieje domniemany parametr „this”, czyli obiekt, dla którego wywoływana jest metoda. Wszystkie dane wejściowe funkcji klasy typu są jawne.
  • Implementację interfejsu należy zdefiniować jako część klasy implementującej interfejs. I odwrotnie, „instancja” klasy typu może być zdefiniowana całkowicie oddzielnie od skojarzonego z nią typu ... nawet w innym module.

Generalnie myślę, że można uczciwie powiedzieć, że klasy typów są bardziej wydajne i elastyczne niż interfejsy. Jak zdefiniowałbyś interfejs do konwersji ciągu znaków na jakąś wartość lub instancję typu implementującego? Z pewnością nie jest to niemożliwe, ale wynik nie byłby intuicyjny ani elegancki. Czy kiedykolwiek marzyłeś o możliwości zaimplementowania interfejsu dla typu w jakiejś skompilowanej bibliotece? Oba są łatwe do wykonania za pomocą klas typów.


1
Jak rozszerzyłbyś typeklasy? Czy typeklasy mogą rozszerzać inne typeklasy, tak jak interfejsy mogą rozszerzać interfejsy?
CMCDragonkai,

10
Warto zaktualizować tę odpowiedź w świetle domyślnych implementacji Java 8 w interfejsach.
Przywróć Monikę

1
@CMCDragonkai Tak, możesz na przykład powiedzieć „class (Foo a) => Bar a where ...”, aby określić, że klasa typu Bar rozszerza klasę typu Foo. Podobnie jak Java, Haskell ma tutaj wielokrotne dziedziczenie.
Przywróć Monikę

W tym przypadku klasa typu nie jest taka sama jak protokoły w Clojure, ale z bezpieczeństwem typów?
nawfal

24

Klasy typów zostały utworzone jako ustrukturyzowany sposób wyrażenia „polimorfizmu ad hoc”, który jest w zasadzie terminem technicznym określającym przeciążone funkcje . Definicja klasy typu wygląda mniej więcej tak:

class Foobar a where
    foo :: a -> a -> Bool
    bar :: String -> a

Oznacza to, że kiedy używasz funkcji Apply foodo niektórych argumentów typu, które należą do klasy Foobar, wyszukuje ona implementację foospecyficzną dla tego typu i używa jej. Jest to bardzo podobne do sytuacji z przeciążeniem operatorów w językach takich jak C ++ / C #, z wyjątkiem bardziej elastycznych i uogólnionych.

Interfejsy służą podobnemu celowi w językach OO, ale podstawowa koncepcja jest nieco inna; Języki OO mają wbudowane pojęcie hierarchii typów, których Haskell po prostu nie ma, co w pewien sposób komplikuje sprawę, ponieważ interfejsy mogą obejmować zarówno przeciążanie przez podtypy (tj. Wywoływanie metod w odpowiednich instancjach, podtypy implementujące interfejsy ich nadtypów) i przez płaskie rozsyłanie oparte na typach (ponieważ dwie klasy implementujące interfejs mogą nie mieć wspólnej nadklasy, która również ją implementuje). Biorąc pod uwagę ogromną dodatkową złożoność wprowadzoną przez podtypy, sugeruję, że bardziej pomocne jest myślenie o klasach typów jako ulepszonej wersji przeciążonych funkcji w języku innym niż OO.

Warto również zauważyć, że klasy typów mają znacznie bardziej elastyczne sposoby rozsyłania - interfejsy zazwyczaj dotyczą tylko pojedynczej klasy, która ją implementuje, podczas gdy klasy typów są definiowane dla typu , który może pojawić się w dowolnym miejscu w sygnaturze funkcji klasy. Odpowiednikiem tego w interfejsach OO byłoby umożliwienie interfejsowi definiowania sposobów przekazywania obiektu tej klasy do innych klas, definiowania statycznych metod i konstruktorów, które wybierałyby implementację na podstawie tego, jaki typ zwracania jest wymagany w kontekście wywoływania, definiowania metod, które przyjmować argumenty tego samego typu, co klasa implementująca interfejs i różne inne rzeczy, które tak naprawdę nie tłumaczą.

Krótko mówiąc: służą podobnym celom, ale sposób ich działania jest nieco inny, a klasy typów są zarówno znacznie bardziej wyraziste, jak i, w niektórych przypadkach, prostsze w użyciu, ponieważ pracują nad stałymi typami, a nie elementami hierarchii dziedziczenia.


Walczyłem ze zrozumieniem hierarchii typów w Haskell, co myślisz o systemach typów, takich jak Omega? Czy mogą symulować hierarchie typów?
CMCDragonkai,

@CMCDragonkai: Nie jestem wystarczająco zaznajomiony z Omegą, aby naprawdę powiedzieć, przepraszam.
CA McCann

16

Przeczytałem powyższe odpowiedzi. Czuję, że mogę odpowiedzieć nieco jaśniej:

„Klasa typu” Haskella i „interfejs” Java / C # lub „cecha” Scali są w zasadzie analogiczne. Nie ma między nimi rozróżnienia koncepcyjnego, ale istnieją różnice w implementacji:

  • Klasy typu Haskell są implementowane z „instancjami”, które są niezależne od definicji typu danych. W C # / Java / Scala interfejsy / cechy muszą być zaimplementowane w definicji klasy.
  • Klasy typu Haskell umożliwiają zwrócenie tego typu lub typu własnego. Cechy Scala również się sprawdzają (ten typ). Zwróć uwagę, że „typy siebie” w Scali to zupełnie niezwiązana funkcja. Java / C # wymaga niechlujnego obejścia za pomocą typów ogólnych, aby przybliżyć to zachowanie.
  • Klasy typu Haskell pozwalają definiować funkcje (w tym stałe) bez wejściowego parametru typu „this”. Interfejsy Java / C # i cechy Scala wymagają parametru wejściowego „this” we wszystkich funkcjach.
  • Klasy typu Haskell umożliwiają definiowanie domyślnych implementacji funkcji. Podobnie jak cechy Scala i interfejsy Java 8+. C # może przybliżyć coś takiego za pomocą metod rozszerzeń.

2
Aby dodać trochę literatury do tych punktów odpowiedzi, przeczytałem dziś On (Haskell) Type Classes and (C #) Interfaces , który również porównuje się z interfejsami C # zamiast Javas, ale powinien dać zrozumienie od strony koncepcyjnej, analizując pojęcie interfejs ponad granicami językowymi.
daniel.kahlenberg

Myślę, że niektóre przybliżenia rzeczy, takich jak implementacje domyślne, są wykonywane raczej wydajniej w C # z klasami abstrakcyjnymi, być może?
Arwin

12

W Master minds of Programming jest wywiad o Haskellu z Philem Wadlerem, wynalazcą klas typów, który wyjaśnia podobieństwa między interfejsami w Javie i klasami typów w Haskell:

Metoda Java, taka jak:

   public static <T extends Comparable<T>> T min (T x, T y) 
   {
      if (x.compare(y) < 0)
            return x; 
      else
            return y; 
   }

jest bardzo podobny do metody Haskella:

   min :: Ord a => a -> a -> a
   min x y  = if x < y then x else y

Tak więc klasy typów są powiązane z interfejsami, ale rzeczywista zgodność byłaby metodą statyczną sparametryzowaną typem jak powyżej.


To powinna być odpowiedź, ponieważ według strony domowej Phila Wadlera był głównym projektantem Haskella, jednocześnie zaprojektował również rozszerzenie Generics dla Javy, które później zostało włączone do samego języka.
Wong Jia Hau


8

Przeczytaj artykuł dotyczący rozszerzenia oprogramowania i integracji z klasami typów, gdzie podano przykłady, w jaki sposób klasy typów mogą rozwiązać wiele problemów, których interfejsy nie mogą.

Przykłady wymienione w artykule to:

  • problem z ekspresją,
  • problem integracji ram,
  • problem niezależnej rozszerzalności,
  • tyrania dominującego rozkładu, rozpraszania i plątania się.

3
Link nie żyje powyżej. Spróbuj zamiast tego .
Justin Leitgeb

1
Cóż, teraz ten też nie żyje. Spróbuj tego
RichardW

7

Nie mogę mówić na poziomie „szumu”, jeśli wydaje mi się to w porządku. Ale klasy typu tak są podobne pod wieloma względami. Jedyną różnicą, o której przychodzi mi do głowy, jest to, że Haskell możesz zapewnić zachowanie niektórych operacji klasy typu :

class  Eq a  where
  (==), (/=) :: a -> a -> Bool
  x /= y     = not (x == y)
  x == y     = not (x /= y)

co pokazuje, że istnieją dwie operacje równe (==)i nierówne (/=)dla rzeczy, które są instancjami Eqklasy typu. Ale operacja nierówna jest definiowana w kategoriach równych (tak, że musisz podać tylko jedną) i odwrotnie.

Więc w prawdopodobnie-nie-legalnej-Javie byłoby to coś takiego:

interface Equal<T> {
    bool isEqual(T other) {
        return !isNotEqual(other); 
    }

    bool isNotEqual(T other) {
        return !isEqual(other); 
    }
}

a sposób, w jaki to działałoby, polega na tym, że wystarczy podać jedną z tych metod, aby zaimplementować interfejs. Powiedziałbym więc, że możliwość zapewnienia pewnego rodzaju częściowej implementacji pożądanego zachowania na poziomie interfejsu jest różnicą.


6

Są podobne (czytaj: mają podobne zastosowanie) i prawdopodobnie zaimplementowane w podobny sposób: funkcje polimorficzne w Haskell biorą pod maskę „tabelę vtable” zawierającą funkcje powiązane z typeklasą.

Tę tabelę można często wywnioskować w czasie kompilacji. Jest to prawdopodobnie mniej prawdziwe w Javie.

Ale to jest tabela funkcji , a nie metod . Metody są powiązane z obiektem, typeklasy Haskella nie.

Zobacz je raczej jak typy generyczne Javy.


3

Jak mówi Daniel, implementacje interfejsów są definiowane oddzielnie od deklaracji danych. Jak zauważyli inni, istnieje prosty sposób definiowania operacji używających tego samego dowolnego typu w więcej niż jednym miejscu. Więc jest to łatwe do zdefiniowaniaNum jako typeklasę. W ten sposób w Haskell otrzymujemy składniowe korzyści wynikające z przeciążenia operatorów bez faktycznego posiadania żadnych magicznych operatorów przeciążonych - tylko standardowe typeklasy.

Inną różnicą jest to, że możesz używać metod opartych na typie, nawet jeśli nie masz jeszcze konkretnej wartości tego typu!

Na przykład read :: Read a => String -> a. Więc jeśli masz wystarczająco dużo innych informacji na temat tego, jak użyjesz wyniku „odczytu”, możesz pozwolić kompilatorowi dowiedzieć się, którego słownika użyć za Ciebie.

Możesz także zrobić takie rzeczy, jak to instance (Read a) => Read [a] where...pozwala zdefiniować wystąpienie odczytu dla dowolnego pliku listy czytelnych rzeczy. Myślę, że nie jest to całkiem możliwe w Javie.

A wszystko to to tylko standardowe typeklasy jednoparametrowe, bez żadnych sztuczek. Gdy wprowadzimy wieloparametrowe typeklasy, otwiera się zupełnie nowy świat możliwości, a tym bardziej z zależnościami funkcjonalnymi i rodzinami typów, które pozwalają osadzić znacznie więcej informacji i obliczeń w systemie typów.


1
Normalne klasy typu są przeznaczone dla interfejsów, tak jak klasy typu wieloparametrowego są przeznaczone do wielokrotnego wysyłania w trybie OOP; zyskujesz odpowiedni wzrost mocy zarówno języka programowania, jak i bólu głowy programisty.
CA McCann
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.