Rozważmy Functor
klasę typu w Haskell, gdzie f
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
od a
do b
, ale pozostawia f
taki, jaki był. Więc jeśli używasz fmap
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órej map
metoda zwraca inny rodzaj kolekcji lub nawet coś, co w ogóle nie jest kolekcją, ale nadal jest Functor
. Ponadto, gdy używasz map
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:
- System typów nie pozwala nam wyrazić niezmiennej, że
map
metoda zawsze zwraca tę samą Functor
podklasę co odbiornik.
- W związku z tym nie ma statycznego bezpiecznego sposobu wywoływania
Functor
metody 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, 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
z map
metody, ale ponieważ nie ma ograniczeń co do liczby Functor
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:
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 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 same F
jak FA
-tak że kiedy zadeklarować typ List<A> implements Functor<List<A>, A>
The map
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 prostu List
lub 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 T
jest zmienną typu, możesz pisać T
i 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 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ę.
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 z Seq
, operację mapy otrzymasz „za darmo” przez ponowne użycie Seq.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 jak IO
akcje, 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 fmap
zachować ten typ - ponieważ gdy tylko otrzymasz map
operacje, które dają inny typ wyniku, wykonanie takich gwarancji staje się dużo, dużo trudniejsze.