Rozważmy Functorklasę typu w Haskell, gdzie fjest 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 fod ado b, ale pozostawia ftaki, jaki był. Więc jeśli używasz fmaplisty, otrzymujesz listę, jeśli używasz jej przez parser, otrzymujesz parser i tak dalej. A są to statyczne gwarancje czasu kompilacji.
Nie znam Functorję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órej mapmetoda zwraca inny rodzaj kolekcji lub nawet coś, co w ogóle nie jest kolekcją, ale nadal jest Functor. Ponadto, gdy używasz mapmetody, 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:
- System typów nie pozwala nam wyrazić niezmiennej, że
mapmetoda zawsze zwraca tę samą Functorpodklasę co odbiornik.
- W związku z tym nie ma statycznego bezpiecznego sposobu wywoływania
Functormetody innej niż wynik map.
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, Functorktó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 Functorz mapmetody, ale ponieważ nie ma ograniczeń co do liczby Functormoż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:
interface Semigroup<T extends Semigroup<T>> {
T append(T arg);
}
class Foo implements Semigroup<Foo> {
Foo append(Foo arg);
}
class Bar implements Semigroup<Bar> {
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 Functorograniczenia:
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 FBsię mieć takie same Fjak FA-tak że kiedy zadeklarować typ List<A> implements Functor<List<A>, A>The mapmetoda 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 Fzostanie utworzona instancja nieparametryzowanych typów, takich jak po prostu Listlub Map. Gwarantuje to, że a FunctorStrategy<List>może zwrócić tylko a List- 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 Tjest zmienną typu, możesz pisać Ti List<T>, ale nie T<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> {
<B> List<B> map(Function<A, B> f) {
}
}
W szczególności odnosząc się do tego fragmentu:
(Myślę) Rozumiem, że zamiast myList |> List.map flub myList |> Seq.map f |> Seq.toListwyższego rodzaju typy pozwalają po prostu napisać myList |> map fi 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 Seqi tak, a potem mogę konwertować na cokolwiek chcę.
Istnieje wiele języków, które uogólniają ideę mapfunkcji 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 z Seq, operację mapy otrzymasz „za darmo” przez ponowne użycie Seq.map.
Jednak u Haskella Functorklasa jest bardziej ogólna; nie jest związane z pojęciem sekwencji. Możesz zaimplementować fmapdla typów, które nie mają dobrego mapowania do sekwencji, takich jak IOakcje, kombinatory parsera, funkcje itp .:
instance Functor IO where
fmap f action =
do x <- action
return (f x)
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g)
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:
- Pierwsze prawo mówi, że mapowanie z funkcją tożsamości / noop jest tym samym, co nie robienie niczego.
- Drugie prawo mówi, że każdy wynik, który można uzyskać poprzez mapowanie dwukrotnie, można również uzyskać przez mapowanie raz.
Dlatego chcesz fmapzachować ten typ - ponieważ gdy tylko otrzymasz mapoperacje, które dają inny typ wyniku, wykonanie takich gwarancji staje się dużo, dużo trudniejsze.