Od jakiegoś czasu zajmuję się programowaniem w języku F # i podoba mi się to. Jednak jedno słowo, które wiem, że nie istnieje w F #, to typy wyższego rodzaju. Przeczytałem materiały o typach wyższego rzędu i myślę, że rozumiem ich definicję. Po prostu nie jestem pewien, dlaczego są przydatne. Czy ktoś może podać przykłady tego, co typy wyższego rodzaju ułatwiają w Scala lub Haskell, które wymagają obejść w języku F #? Również w tych przykładach, jakie byłyby obejścia bez typów wyższego rodzaju (lub odwrotnie w F #)? Może jestem tak przyzwyczajony do pracy nad tym, że nie zauważam braku tej funkcji.
(Myślę) Rozumiem, że zamiast myList |> List.map f
lub myList |> Seq.map f |> Seq.toList
wyższego rodzaju typy pozwalają po prostu napisać myList |> map f
i zwróci List
. To świetnie (zakładając, że to prawda), ale wydaje się trochę małostkowe? (I czy nie można tego zrobić po prostu zezwalając na przeciążanie funkcji?) Zwykle konwertuję na Seq
i tak, a potem mogę konwertować na cokolwiek chcę. Znowu, może jestem po prostu zbyt przyzwyczajony do tego. Ale czy jest jakiś przykład, w którym typy wyższego rzędu naprawdę oszczędzają cię w przypadku naciśnięć klawiszy lub bezpieczeństwa typów?
IMonad<T>
a następnie przesłać go z powrotem np.IEnumerable<int>
LubIObservable<int>
kiedy skończysz? Czy to wszystko tylko po to, by uniknąć rzucania?return
by to działało, ponieważ tak naprawdę należy do typu monady, a nie do konkretnej instancji, więc w ogóle nie chciałbyś umieszczać go wIMonad
interfejsie.bind
akaSelectMany
itp. Co oznacza, że ktoś mógłby użyć API dobind
anIObservable
to anIEnumerable
i założyć, że zadziała, co tak, fuj, jeśli tak jest i nie ma sposobu na obejście tego. Po prostu nie mam 100% pewności, że nie da się tego obejść.Odpowiedzi:
Więc rodzaj typu jest jego prostym typem. Na przykład
Int
ma rodzaj,*
co oznacza, że jest to typ podstawowy i można go utworzyć za pomocą wartości. Według pewnej luźnej definicji typu wyższego rodzaju (i nie jestem pewien, gdzie F # rysuje linię, więc po prostu ją uwzględnij) kontenery polimorficzne są doskonałym przykładem typu wyższego rodzaju.data List a = Cons a (List a) | Nil
Konstruktor typu
List
ma rodzaj,* -> *
co oznacza, że musi zostać przekazany konkretny typ, aby uzyskać konkretny typ:List Int
może mieć mieszkańców takich jak,[1,2,3]
aleList
sam nie może.Zakładam, że zalety polimorficznych pojemników są oczywiste, ale istnieją bardziej przydatne
* -> *
typy niż tylko pojemniki. Na przykład relacjedata Rel a = Rel (a -> a -> Bool)
lub parsery
data Parser a = Parser (String -> [(a, String)])
obaj też są mili
* -> *
.Możemy jednak pójść dalej w Haskell, mając typy z jeszcze wyższymi rodzajami. Na przykład moglibyśmy szukać typu z rodzajem
(* -> *) -> *
. Prostym przykładem może byćShape
próba napełnienia pojemnika* -> *
.data Shape f = Shape (f ()) [(), (), ()] :: Shape List
Jest to przydatne na przykład do charakteryzowania
Traversable
s w Haskellu, ponieważ zawsze można je podzielić według kształtu i zawartości.split :: Traversable t => t a -> (Shape t, [a])
Jako inny przykład rozważmy drzewo, które jest sparametryzowane na rodzaj gałęzi, które ma. Na przykład może nim być normalne drzewo
data Tree a = Branch (Tree a) a (Tree a) | Leaf
Ale widzimy, że typ gałęzi zawiera a
Pair
zTree a
s, więc możemy wyodrębnić ten fragment z typu parametryczniedata TreeG f a = Branch a (f (TreeG f a)) | Leaf data Pair a = Pair a a type Tree a = TreeG Pair a
Ten
TreeG
konstruktor typu ma rodzaj(* -> *) -> * -> *
. Możemy go użyć do tworzenia interesujących innych odmian, takich jak aRoseTree
type RoseTree a = TreeG [] a rose :: RoseTree Int rose = Branch 3 [Branch 2 [Leaf, Leaf], Leaf, Branch 4 [Branch 4 []]]
Lub patologiczne, takie jak
MaybeTree
data Empty a = Empty type MaybeTree a = TreeG Empty a nothing :: MaybeTree a nothing = Leaf just :: a -> MaybeTree a just a = Branch a Empty
Lub a
TreeTree
type TreeTree a = TreeG Tree a treetree :: TreeTree Int treetree = Branch 3 (Branch Leaf (Pair Leaf Leaf))
Innym miejscem, w którym się to pojawia, są „algebry funktorów”. Jeśli upuścimy kilka warstw abstrakcji, można to lepiej uznać za fałdę, na przykład
sum :: [Int] -> Int
. Algebry są parametryzowane względem funktora i nośnej . Funktor ma rodzaj* -> *
i rodzaj nośnika*
tak zupełniedata Alg f a = Alg (f a -> a)
ma miły
(* -> *) -> * -> *
.Alg
użyteczne ze względu na związek z typami danych i schematami rekursji zbudowanymi na nich.-- | The "single-layer of an expression" functor has kind `(* -> *)` data ExpF x = Lit Int | Add x x | Sub x x | Mult x x -- | The fixed point of a functor has kind `(* -> *) -> *` data Fix f = Fix (f (Fix f)) type Exp = Fix ExpF exp :: Exp exp = Fix (Add (Fix (Lit 3)) (Fix (Lit 4))) -- 3 + 4 fold :: Functor f => Alg f a -> Fix f -> a fold (Alg phi) (Fix f) = phi (fmap (fold (Alg phi)) f)
Wreszcie, chociaż teoretycznie są one możliwe, nigdy nie widziałem konstruktora typu nawet wyższego rodzaju. Czasami widzimy funkcje tego typu, takie jak
mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b
, ale myślę, że będziesz musiał zagłębić się w prolog typów lub literaturę zależnie wpisaną w typowanie, aby zobaczyć ten poziom złożoności typów.źródło
TreeTree
jest po prostu patologiczne, ale bardziej praktycznie oznacza, że masz dwa różne typy drzew przeplatających się między sobą - posunięcie tego pomysłu nieco dalej może dać ci bardzo potężne pojęcia bezpieczne dla typów, takie jak statycznie bezpieczne czerwone / czarne drzewa i zgrabny statycznie wyważony typ FingerTree.Rozważmy
Functor
klasę typu w Haskell, gdzief
jest zmienną typu wyższego rzędu:class Functor f where fmap :: (a -> b) -> f a -> f b
Ten podpis typu mówi, że fmap zmienia parametr typu z
f
oda
dob
, ale pozostawiaf
taki, jaki był. Więc jeśli używaszfmap
listy, otrzymujesz listę, jeśli używasz jej przez parser, otrzymujesz parser i tak dalej. A są to statyczne gwarancje czasu kompilacji.Nie znam
Functor
języka F #, ale zastanówmy się, co się stanie, jeśli spróbujemy wyrazić abstrakcję w języku takim jak Java lub C #, z dziedziczeniem i rodzajami ogólnymi, ale bez typów ogólnych wyższego rzędu. Pierwsza próba:interface Functor<A> { Functor<B> map(Function<A, B> f); }
Problem z tą pierwszą próbą polega na tym, że implementacja interfejsu może zwrócić dowolną klasę, która implementuje
Functor
. Ktoś mógłby napisać metodę,FunnyList<A> implements Functor<A>
którejmap
metoda zwraca inny rodzaj kolekcji lub nawet coś, co w ogóle nie jest kolekcją, ale nadal jestFunctor
. Ponadto, gdy używaszmap
metody, nie możesz wywołać żadnych metod specyficznych dla podtypu na wyniku, chyba że zmniejszysz go do typu, którego faktycznie oczekujesz. Mamy więc dwa problemy:map
metoda zawsze zwraca tę samąFunctor
podklasę co odbiornik.Functor
metody innej niż wynikmap
.Istnieją inne, bardziej skomplikowane sposoby, które możesz wypróbować, ale żaden z nich tak naprawdę nie działa. Na przykład możesz spróbować rozszerzyć pierwszą próbę, definiując podtypy,
Functor
które ograniczają typ wyniku:interface Collection<A> extends Functor<A> { Collection<B> map(Function<A, B> f); } interface List<A> extends Collection<A> { List<B> map(Function<A, B> f); } interface Set<A> extends Collection<A> { Set<B> map(Function<A, B> f); } interface Parser<A> extends Functor<A> { Parser<B> map(Function<A, B> f); } // …
Pomaga to zabronić implementatorom tych węższych interfejsów zwracania niewłaściwego typu
Functor
zmap
metody, ale ponieważ nie ma ograniczeń co do liczbyFunctor
możliwych implementacji, nie ma ograniczeń co do liczby węższych interfejsów, których będziesz potrzebować.( EDYCJA: Zauważ, że to działa tylko dlatego, że
Functor<B>
pojawia się jako typ wyniku, więc interfejsy potomne mogą go zawęzić. Więc AFAIK nie możemy zawęzić obu zastosowańMonad<B>
w następującym interfejsie:interface Monad<A> { <B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f); }
W Haskell, ze zmiennymi typu wyższego rzędu, jest to
(>>=) :: Monad m => m a -> (a -> m b) -> m b
.)Jeszcze inną próbą jest użycie rekurencyjnych typów ogólnych w celu ograniczenia przez interfejs typu wyniku podtypu do samego podtypu. Przykład zabawki:
/** * A semigroup is a type with a binary associative operation. Law: * * > x.append(y).append(z) = x.append(y.append(z)) */ interface Semigroup<T extends Semigroup<T>> { T append(T arg); } class Foo implements Semigroup<Foo> { // Since this implements Semigroup<Foo>, now this method must accept // a Foo argument and return a Foo result. Foo append(Foo arg); } class Bar implements Semigroup<Bar> { // Any of these is a compilation error: Semigroup<Bar> append(Semigroup<Bar> arg); Semigroup<Foo> append(Bar arg); Semigroup append(Bar arg); Foo append(Bar arg); }
Ale ten rodzaj techniki (który jest raczej tajemniczy dla twojego zwykłego programisty OOP, do cholery również dla twojego zwykłego programisty funkcjonalnego) nadal nie może wyrazić pożądanego
Functor
ograniczenia:interface Functor<FA extends Functor<FA, A>, A> { <FB extends Functor<FB, B>, B> FB map(Function<A, B> f); }
Tutaj problemem jest to nie ogranicza
FB
się mieć takie sameF
jakFA
-tak że kiedy zadeklarować typList<A> implements Functor<List<A>, A>
Themap
metoda może nadal zwracająNotAList<B> implements Functor<NotAList<B>, B>
.Ostatnia próba, w Javie, przy użyciu typów surowych (nieparametryzowanych kontenerów):
interface FunctorStrategy<F> { F map(Function f, F arg); }
Tutaj
F
zostanie utworzona instancja nieparametryzowanych typów, takich jak po prostuList
lubMap
. Gwarantuje to, że aFunctorStrategy<List>
może zwrócić tylko aList
- ale zrezygnowałeś z używania zmiennych typu do śledzenia typów elementów list.Sedno problemu polega na tym, że języki takie jak Java i C # nie pozwalają parametrom typu mieć parametrów. W Javie, jeśli
T
jest zmienną typu, możesz pisaćT
iList<T>
, ale nieT<String>
. Typy wyższego rzędu usuwają to ograniczenie, abyś mógł mieć coś takiego (nie do końca przemyślane):interface Functor<F, A> { <B> F<B> map(Function<A, B> f); } class List<A> implements Functor<List, A> { // Since F := List, F<B> := List<B> <B> List<B> map(Function<A, B> f) { // ... } }
W szczególności odnosząc się do tego fragmentu:
Istnieje wiele języków, które uogólniają ideę
map
funkcji w ten sposób, modelując ją tak, jakby w istocie mapowanie dotyczyło sekwencji. Ta twoja uwaga jest w tym duchu: jeśli masz typ, który obsługuje konwersję do i zSeq
, operację mapy otrzymasz „za darmo” przez ponowne użycieSeq.map
.Jednak u Haskella
Functor
klasa jest bardziej ogólna; nie jest związane z pojęciem sekwencji. Możesz zaimplementowaćfmap
dla typów, które nie mają dobrego mapowania do sekwencji, takich jakIO
akcje, kombinatory parsera, funkcje itp .:instance Functor IO where fmap f action = do x <- action return (f x) -- This declaration is just to make things easier to read for non-Haskellers newtype Function a b = Function (a -> b) instance Functor (Function a) where fmap f (Function g) = Function (f . g) -- `.` is function composition
Pojęcie „mapowania” tak naprawdę nie jest związane z sekwencjami. Najlepiej zrozumieć prawa funktora:
(1) fmap id xs == xs (2) fmap f (fmap g xs) = fmap (f . g) xs
Bardzo nieformalnie:
Dlatego chcesz
fmap
zachować ten typ - ponieważ gdy tylko otrzymaszmap
operacje, które dają inny typ wyniku, wykonanie takich gwarancji staje się dużo, dużo trudniejsze.źródło
fmap
włączone,Function a
gdy ma już.
operację? Rozumiem, dlaczego.
warto zdefiniowaćfmap
operację, ale po prostu nie docieram tam, gdzie kiedykolwiek trzeba było użyćfmap
zamiast.
. Może gdybyś mógł podać przykład, w którym byłoby to przydatne, pomogłoby mi to zrozumieć.double
z funktora, gdziedouble [1, 2, 3]
daje[2, 4, 6]
idouble sin
daje fn, które jest podwójną wartością grzechu. Widzę, gdzie, jeśli zaczniesz myśleć w ten sposób, kiedy uruchomisz mapę na tablicy, spodziewasz się, że tablica z powrotem, a nie tylko sekwencja, ponieważ, cóż, pracujemy tutaj nad tablicami.Functor
i umożliwienia klientowi biblioteki wybrania go. Odpowiedź J. Abrahamsona dostarcza jednego przykładu: fałdy rekurencyjne można uogólnić za pomocą funktorów. Innym przykładem są darmowe monady; można o nich myśleć jako o rodzajach bibliotek implementacyjnych interpretera generycznego, w których klient dostarcza „zestaw instrukcji” jako dowolnyFunctor
.Functor
lubSemiGroup
. Gdzie prawdziwe programy najczęściej używają tej funkcji językowej?Nie chcę tu powtarzać informacji w niektórych doskonałych odpowiedziach, ale jest kluczowy punkt, który chciałbym dodać.
Zwykle nie potrzebujesz typów wyższego rzędu, aby zaimplementować jakąkolwiek konkretną monadę lub funktor (lub funktor aplikacyjny, strzałkę lub ...). Ale takie postępowanie w większości mija się z celem.
Generalnie odkryłem, że kiedy ludzie nie widzą użyteczności funktorów / monad / cokolwiek, często dzieje się tak dlatego, że myślą o tych rzeczach pojedynczo . Operacje Functor / monad / etc naprawdę nie dodają nic do żadnej instancji (zamiast wywoływać bind, fmap, itp., Mogę po prostu wywołać dowolne operacje, których użyłem do zaimplementowania bind, fmap itp.). To, czego naprawdę potrzebujesz, to abyś mógł mieć kod, który działa ogólnie z dowolnym funktorem / monadą / itp.
W kontekście, w którym taki ogólny kod jest szeroko stosowany, oznacza to, że za każdym razem, gdy piszesz nową instancję monady, twój typ natychmiast uzyskuje dostęp do dużej liczby użytecznych operacji, które zostały już dla ciebie napisane . O to właśnie chodzi, by wszędzie widzieć monady (i funktory i ...); nie po to, żebym mógł używać
bind
zamiastconcat
imap
implementowaćmyFunkyListOperation
(co samo w sobie nie daje mi nic), ale raczej po to, aby kiedy potrzebowaćmyFunkyParserOperation
imyFunkyIOOperation
móc ponownie użyć kodu, który pierwotnie widziałem w kategoriach list, ponieważ jest to faktycznie monada .Ale aby abstrahować od sparametryzowanego typu, takiego jak monada z bezpieczeństwem typów , potrzebujesz typów wyższego rodzaju (jak również wyjaśniono w innych odpowiedziach tutaj).
źródło
Z perspektywy bardziej specyficznej dla platformy .NET, jakiś czas temu napisałem o tym wpis na blogu . Sedno tego polega na tym , że w przypadku typów wyższego rodzaju możesz potencjalnie ponownie użyć tych samych bloków LINQ między
IEnumerables
iIObservables
, ale bez typów wyższego rodzaju jest to niemożliwe.Najbliższy można dostać (ja zorientowali się po zaksięgowaniu bloga) to zrobić własny
IEnumerable<T>
iIObservable<T>
i rozszerzył je zarówno zIMonad<T>
. To pozwala na ponowne użycie bloków LINQ, jeśli są one oznaczoneIMonad<T>
, ale wtedy to już nie typesafe ponieważ pozwala na mix-and-matchIObservables
iIEnumerables
w tym samym bloku, który choć może to brzmieć intrygujące zezwala na to, że jesteś po prostu uzyskaj niezdefiniowane zachowanie.Napisałem później post o tym, jak Haskell to ułatwia. (No-op, naprawdę - ograniczenie bloku do pewnego rodzaju monady wymaga kodu; opcja ponownego użycia jest domyślna).
źródło
IObservables
w kodzie produkcyjnym.IObservable
i używasz zdarzeń w rozdziale WinForms swojej własnej książki.Najczęściej używanym przykładem polimorfizmu typu wyższego rzędu w Haskell jest
Monad
interfejs.Functor
iApplicative
są w ten sam sposób wyższego rodzaju, więc pokażęFunctor
, aby pokazać coś zwięzłego.class Functor f where fmap :: (a -> b) -> f a -> f b
Teraz przyjrzyj się tej definicji, przyglądając się, jak
f
używana jest zmienna typu . Zobaczysz, żef
nie może to oznaczać typu, który ma wartość. Możesz zidentyfikować wartości w tym podpisie typu, ponieważ są one argumentami funkcji i wynikami. Więc zmienne typua
ib
są typami, które mogą mieć wartości. Podobnie jest z wyrażeniami typuf a
if b
. Ale nief
siebie.f
jest przykładem zmiennej typu wyższego rzędu. Biorąc pod uwagę, że*
jest to rodzaj typów, które mogą mieć wartości,f
muszą mieć ten rodzaj* -> *
. Oznacza to, że przyjmuje typ, który może mieć wartości, ponieważ wiemy z poprzedniego badania, żea
ib
musi mieć wartości. Wiemy też, żef a
if b
musi mieć wartości, więc zwraca typ, który musi mieć wartości.To sprawia, że jest
f
używany w definicjiFunctor
zmiennej typu wyższego rodzaju.Te
Applicative
iMonad
interfejsy dodać więcej, ale są one kompatybilne. Oznacza to, że działają również na zmiennych typu z rodzajem* -> *
.Praca nad typami wyższego rzędu wprowadza dodatkowy poziom abstrakcji - nie jesteś ograniczony tylko do tworzenia abstrakcji na typach podstawowych. Możesz także tworzyć abstrakcje dla typów, które modyfikują inne typy.
źródło