Dlaczego potrzebujesz wyższych rodzajów?

13

Niektóre języki dopuszczają klasy i funkcje z parametrami typu (np. List<T>Gdzie Tmoże być dowolnym typem). Na przykład możesz mieć funkcję:

List<S> Function<S, T>(List<T> list)

Niektóre języki umożliwiają jednak rozszerzenie tej koncepcji o jeden poziom wyżej, co pozwala na posiadanie funkcji z podpisem:

K<S> Function<K<_>, S, T>(K<T> arg)

Gdzie K<_>sam jest typem takim, List<_>który ma parametr typu. Ten „częściowy typ” jest znany jako konstruktor typów.

Moje pytanie brzmi: dlaczego potrzebujesz tej umiejętności? Sensowne jest mieć taki typ, List<T>ponieważ wszystkie List<T>są prawie dokładnie takie same, ale wszystkie K<_>mogą być zupełnie inne. Możesz mieć Option<_>i List<_>, które nie mają żadnej wspólnej funkcjonalności.

GregRos
źródło
7
Istnieje kilka dobrych odpowiedzi tutaj: stackoverflow.com/questions/21170493/…
itsbruce
3
@itsbruce W szczególności Functorprzykład w odpowiedzi Luisa Casillasa jest dość intuicyjny. Co mają List<T>i Option<T>mają wspólnego? Jeśli dasz mi jedną i jedną funkcję T -> S, mogę ci dać List<S>lub Option<S>. Kolejną wspólną cechą jest to, że możesz spróbować uzyskać Twartość z obu.
Doval,
@Doval: Jak zrobiłbyś to pierwsze? W stopniu, w jakim interesuje się tym drugim, sądzę, że można to rozwiązać poprzez wdrożenie obu typów IReadableHolder<T>.
supercat
@supercat Zgaduję musieliby realizować IMappable<K<_>, T>metodą K<S> Map(Func<T, S> f), wykonawcze IMappable<Option<_>, T>, IMappable<List<_>, T>. Musiałbyś więc ograniczyć się, K<T> : IMappable<K<_>, T>aby z tego skorzystać.
GregRos
1
Przesyłanie nie jest odpowiednią analogią. Klasy typów (lub podobne konstrukcje) są wykonywane w taki sposób, aby pokazać, w jaki sposób dany typ spełnia typ wyższego typu. Zazwyczaj obejmuje to zdefiniowanie nowych funkcji lub pokazanie, które z istniejących funkcji (lub metod, jeśli jest to język OO, taki jak Scala), których można użyć. Po wykonaniu tej czynności wszystkie funkcje zdefiniowane do pracy z wyższym typem będą działać z tym typem. Ale to znacznie więcej niż interfejs, ponieważ zdefiniowano więcej niż prosty zestaw sygnatur funkcji / metod. Chyba będę musiał napisać odpowiedź, aby pokazać, jak to działa.
itsbruce

Odpowiedzi:

5

Ponieważ nikt inny nie odpowiedział na to pytanie, myślę, że spróbuję. Będę musiał trochę filozofować.

Programowanie ogólne polega na abstrakcji nad podobnymi typami, bez utraty informacji o typie (co dzieje się z polimorfizmem wartości obiektowych). Aby to zrobić, typy muszą koniecznie współdzielić jakiś interfejs (zestaw operacji, a nie termin OO), którego można użyć.

W językach obiektowych typy spełniają interfejs dzięki klasom. Każda klasa ma własny interfejs zdefiniowany jako część swojego typu. Ponieważ wszystkie klasy List<T>mają ten sam interfejs, możesz pisać kod, który działa bez względu na to, co Twybierzesz. Innym sposobem nałożenia interfejsu jest ograniczenie dziedziczenia i chociaż oba wydają się różne, są one trochę podobne, jeśli się nad tym zastanowić.

W większości języków obiektowych List<>sam w sobie nie jest właściwym typem. Nie ma metod, a zatem nie ma interfejsu. Tylko List<T>to ma te rzeczy. Zasadniczo, z bardziej technicznego punktu widzenia, jedynymi typami, które można znacząco wyodrębnić, są te z tym rodzajem *. Aby korzystać z typów lepiej dobranych w świecie zorientowanym obiektowo, musisz sformułować ograniczenia typu w sposób zgodny z tym ograniczeniem.

Na przykład, jak wspomniano w komentarzach, możemy przeglądać Option<>i List<>„mapować”, w tym sensie, że jeśli masz funkcję, możesz przekonwertować Option<T>na an Option<S>lub List<T>na List<S>. Pamiętając, że klasy nie mogą być używane do abstrakcyjnego nad typami wyższego rodzaju, zamiast tego tworzymy interfejs:

IMappable<K<_>, T> where K<T> : IMappable<K<_>, T>

A następnie zaimplementować interfejs zarówno List<T>a Option<T>jak IMappable<List<_>, T>i IMappable<Option<_>, T>odpowiednio. To, co zrobiliśmy, polega na użyciu typów o wyższym rodzaju, aby nałożyć ograniczenia na rzeczywiste typy (o innym rodzaju) Option<T>i List<T>. Tak to się dzieje w Scali, chociaż oczywiście Scala ma takie cechy, jak cechy, zmienne typu i ukryte parametry, które czynią ją bardziej wyrazistą.

W innych językach można bezpośrednio wyodrębnić typy wyższego rodzaju. W Haskell, jednym z najwyższych uprawnień w systemach typów, możemy sformułować klasę typu dla dowolnego typu, nawet jeśli ma on wyższy rodzaj. Na przykład,

class Mappable mp where
    map :: mp a -> mp b

Jest to ograniczenie nakładane bezpośrednio na (nieokreślony) typ, mpktóry przyjmuje jeden parametr typu i wymaga, aby był on powiązany z funkcją, mapktóra zmienia an mp<a>w mp<b>. Następnie możemy napisać funkcje ograniczające typy wyższego rzędu, Mappabletak jak w językach zorientowanych obiektowo można wprowadzić ograniczenie dziedziczenia. Cóż, w pewnym sensie.

Podsumowując, twoja umiejętność korzystania z typów o wyższym rodzaju zależy od twojej zdolności do ich ograniczenia lub użycia ich jako części ograniczeń typów.

GregRos
źródło
Byłem zbyt zajęty zmianą pracy, ale w końcu znajdę trochę czasu na napisanie własnej odpowiedzi. Myślę jednak, że rozproszyła Cię idea ograniczeń. Jednym z istotnych aspektów wyższych rodzajów rodzajów jest to, że pozwalają one na zdefiniowanie funkcji / zachowania, które można zastosować do dowolnego typu, który logicznie można wykazać, że się kwalifikuje. Daleki od nakładania ograniczeń na istniejące typy, klasy typów (jedna aplikacja wyższych typów) dodają do nich zachowanie.
itsbruce
Myślę, że to kwestia semantyki. Klasa typu definiuje klasę typów. Kiedy definiujesz funkcję taką jak (Mappable mp) => mp a -> mp b, nałożyłeś ograniczenie na mpbycie członkiem klasy typu Mappable. Gdy deklarujesz typ, Optionktóry ma być instancją Mappable, dodajesz zachowanie do tego typu. Sądzę, że możesz skorzystać z tego zachowania lokalnie, nie ograniczając żadnego typu, ale nie różni się to od definiowania zwykłej funkcji.
GregRos
Ponadto jestem pewien, że klasy typów nie są bezpośrednio związane z typami wyższego rodzaju. Scala ma typy lepiej dobrane, ale nie klasy, i można ograniczyć klasy do typów z rodzajem, *nie czyniąc ich bezużytecznymi. Zdecydowanie jest jednak prawdą, że klasy typów są bardzo wydajne podczas pracy z typami wyższego rodzaju.
GregRos
Scala ma klasy typów. Są wdrażane w sposób dorozumiany. Implikacje zapewniają odpowiednik instancji klasy typu Haskell. Jest to bardziej delikatna implementacja niż Haskell, ponieważ nie ma dedykowanej składni. Ale zawsze był to jeden z kluczowych celów implikacji Scali i wykonują swoją pracę.
itsbruce