Betrachten Sie die Functor
Typklasse in Haskell, in der f
sich eine höherwertige Typvariable befindet:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Was diese Art Unterschrift sagt , ist , dass fmap den Typ - Parameter eines ändert sich f
von a
zu b
, aber Blätter f
wie es war. Wenn Sie also fmap
über eine Liste verwenden, erhalten Sie eine Liste, wenn Sie sie über einen Parser verwenden, erhalten Sie einen Parser und so weiter. Und dies sind statische Garantien zur Kompilierungszeit.
Ich kenne F # nicht, aber lassen Sie uns überlegen, was passiert, wenn wir versuchen, die Functor
Abstraktion in einer Sprache wie Java oder C # auszudrücken , mit Vererbung und Generika, aber ohne höherwertige Generika. Erster Versuch:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
Das Problem bei diesem ersten Versuch ist, dass eine Implementierung der Schnittstelle jede implementierte Klasse zurückgeben darf Functor
. Jemand könnte eine schreiben, FunnyList<A> implements Functor<A>
deren map
Methode eine andere Art von Sammlung zurückgibt, oder sogar etwas anderes, das überhaupt keine Sammlung ist, aber immer noch eine Functor
. Wenn Sie die map
Methode verwenden, können Sie für das Ergebnis keine subtypspezifischen Methoden aufrufen, es sei denn, Sie übertragen sie auf den Typ, den Sie tatsächlich erwarten. Wir haben also zwei Probleme:
- Das Typsystem erlaubt es uns nicht, die Invariante auszudrücken, dass die
map
Methode immer dieselbe Functor
Unterklasse wie der Empfänger zurückgibt .
- Daher gibt es keine statisch typsichere Möglichkeit, eine Nichtmethode
Functor
für das Ergebnis von aufzurufen map
.
Es gibt andere, kompliziertere Möglichkeiten, die Sie ausprobieren können, aber keine davon funktioniert wirklich. Sie können beispielsweise versuchen, den ersten Versuch zu erweitern, indem Sie Untertypen definieren Functor
, die den Ergebnistyp einschränken:
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);
}
// …
Dies hilft zu verhindern, dass Implementierer dieser schmaleren Schnittstellen den falschen Typ Functor
von der map
Methode zurückgeben. Da es jedoch keine Begrenzung für die Anzahl der Functor
Implementierungen gibt, die Sie haben können, gibt es keine Begrenzung für die Anzahl der engeren Schnittstellen, die Sie benötigen.
( BEARBEITEN: Und beachten Sie, dass dies nur funktioniert, weil es Functor<B>
als Ergebnistyp angezeigt wird und die untergeordneten Schnittstellen es eingrenzen können. AFAIK Wir können also nicht beide Verwendungen Monad<B>
in der folgenden Schnittstelle eingrenzen:
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
In Haskell mit höherrangigen Variablen ist dies (>>=) :: Monad m => m a -> (a -> m b) -> m b
.)
Ein weiterer Versuch besteht darin, rekursive Generika zu verwenden, um zu versuchen, dass die Schnittstelle den Ergebnistyp des Subtyps auf den Subtyp selbst beschränkt. Spielzeugbeispiel:
/**
* 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);
}
Aber diese Art von Technik (die für Ihren normalen OOP-Entwickler ziemlich geheimnisvoll ist, zum Teufel auch für Ihren normalen funktionalen Entwickler) kann die gewünschte Functor
Einschränkung auch noch nicht ausdrücken :
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
Das Problem hierbei ist, dass dies nicht darauf beschränkt ist FB
, dasselbe zu haben F
wie FA
- so dass List<A> implements Functor<List<A>, A>
die map
Methode beim Deklarieren eines Typs immer noch a zurückgeben kann NotAList<B> implements Functor<NotAList<B>, B>
.
Letzter Versuch in Java unter Verwendung von Rohtypen (nicht parametrisierte Container):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Hier F
werden nicht parametrisierte Typen wie just List
oder instanziiert Map
. Dies garantiert, dass a FunctorStrategy<List>
nur a zurückgeben kann List
- Sie haben jedoch die Verwendung von Typvariablen zum Verfolgen der Elementtypen der Listen aufgegeben.
Das Herzstück des Problems ist, dass Sprachen wie Java und C # nicht zulassen, dass Typparameter Parameter haben. Wenn T
es sich in Java um eine Typvariable handelt, können Sie schreiben T
und List<T>
, aber nicht T<String>
. Höher sortierte Typen heben diese Einschränkung auf, so dass Sie so etwas haben können (nicht vollständig durchdacht):
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) {
// ...
}
}
Und insbesondere dieses Bit ansprechen:
(Ich denke) Ich verstehe, dass Sie anstelle von myList |> List.map f
oder myList |> Seq.map f |> Seq.toList
höherwertigen Typen einfach schreiben können myList |> map f
und es wird a zurückgegeben List
. Das ist großartig (vorausgesetzt es ist richtig), scheint aber irgendwie kleinlich zu sein? (Und könnte es nicht einfach durch Zulassen einer Funktionsüberladung geschehen?) Normalerweise konvertiere ich Seq
trotzdem und kann danach in alles konvertieren, was ich will.
Es gibt viele Sprachen, die die Idee der map
Funktion auf diese Weise verallgemeinern , indem sie sie so modellieren, als ob es bei der Zuordnung im Kern um Sequenzen geht. Ihre Bemerkung ist in diesem Sinne: Wenn Sie einen Typ haben, der die Konvertierung von und nach unterstützt Seq
, erhalten Sie die Kartenoperation "kostenlos", indem Sie sie wiederverwenden Seq.map
.
In Haskell ist die Functor
Klasse jedoch allgemeiner; es ist nicht an den Begriff der Sequenzen gebunden. Sie können fmap
Typen implementieren , die keine gute Zuordnung zu Sequenzen aufweisen, z. B. IO
Aktionen, Parser-Kombinatoren, Funktionen usw.:
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
Das Konzept des "Mappings" ist wirklich nicht an Sequenzen gebunden. Es ist am besten, die Funktorgesetze zu verstehen:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
Sehr informell:
- Das erste Gesetz besagt, dass Mapping mit einer Identity / Noop-Funktion dasselbe ist wie nichts zu tun.
- Das zweite Gesetz besagt, dass jedes Ergebnis, das Sie durch zweimaliges Mapping erzielen können, auch durch einmaliges Mapping erzielt werden kann.
Aus diesem Grund möchten Sie fmap
den Typ beibehalten. Sobald Sie map
Operationen erhalten, die einen anderen Ergebnistyp erzeugen, wird es sehr viel schwieriger, solche Garantien zu geben.