Considérez la Functor
classe de type dans Haskell, où f
est une variable de type de type supérieur:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Ce que dit cette signature de type, c'est que fmap modifie le paramètre de type d'un f
de a
à b
, mais laisse f
tel quel. Donc, si vous utilisez fmap
sur une liste, vous obtenez une liste, si vous l'utilisez sur un analyseur, vous obtenez un analyseur, et ainsi de suite. Et ce sont des garanties statiques au moment de la compilation.
Je ne connais pas F #, mais considérons ce qui se passe si nous essayons d'exprimer l' Functor
abstraction dans un langage comme Java ou C #, avec héritage et génériques, mais pas de génériques de type supérieur. Premier essai:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
Le problème avec ce premier essai est qu'une implémentation de l'interface est autorisée à renvoyer n'importe quelle classe implémentant Functor
. Quelqu'un pourrait écrire une méthode FunnyList<A> implements Functor<A>
dont la map
méthode renvoie un autre type de collection, ou même quelque chose d'autre qui n'est pas du tout une collection mais qui est toujours un Functor
. De plus, lorsque vous utilisez la map
méthode, vous ne pouvez pas invoquer de méthodes spécifiques à un sous-type sur le résultat à moins que vous ne le réduisiez au type que vous attendez réellement. Nous avons donc deux problèmes:
- Le système de types ne nous permet pas d'exprimer l'invariant selon lequel la
map
méthode renvoie toujours la même Functor
sous-classe que le récepteur.
- Par conséquent, il n'y a pas de manière statiquement sûre d'appeler une non-
Functor
méthode sur le résultat de map
.
Il existe d'autres méthodes plus compliquées que vous pouvez essayer, mais aucune d'entre elles ne fonctionne vraiment. Par exemple, vous pouvez essayer d'augmenter le premier essai en définissant des sous-types Functor
qui restreignent le type de résultat:
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);
}
// …
Cela aide à interdire aux implémenteurs de ces interfaces plus étroites de renvoyer le mauvais type de Functor
de la map
méthode, mais comme il n'y a pas de limite au nombre d' Functor
implémentations que vous pouvez avoir, il n'y a pas de limite au nombre d'interfaces plus étroites dont vous aurez besoin.
( EDIT: Et notez que cela ne fonctionne que parce qu'il Functor<B>
apparaît comme le type de résultat, et donc les interfaces enfants peuvent le restreindre. Donc, AFAIK, nous ne pouvons pas restreindre les deux utilisations de Monad<B>
dans l'interface suivante:
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
Dans Haskell, avec des variables de type de rang supérieur, c'est (>>=) :: Monad m => m a -> (a -> m b) -> m b
.)
Une autre tentative consiste à utiliser des génériques récursifs pour essayer de faire en sorte que l'interface restreigne le type de résultat du sous-type au sous-type lui-même. Exemple de jouet:
/**
* 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);
}
Mais ce type de technique (qui est plutôt obscur pour votre développeur OOP ordinaire, diable pour votre développeur fonctionnel ordinaire également) ne peut toujours pas exprimer la Functor
contrainte souhaitée non plus:
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
Le problème ici est que cela ne se limite pas FB
à avoir le même F
que FA
- de sorte que lorsque vous déclarez un type List<A> implements Functor<List<A>, A>
, la map
méthode peut toujours renvoyer un NotAList<B> implements Functor<NotAList<B>, B>
.
Dernier essai, en Java, en utilisant des types bruts (conteneurs non paramétrés):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Ici, F
seront instanciés vers des types non paramétrés comme just List
ou Map
. Cela garantit que a FunctorStrategy<List>
ne peut renvoyer qu'un List
- mais vous avez abandonné l'utilisation de variables de type pour suivre les types d'éléments des listes.
Le cœur du problème ici est que les langages comme Java et C # ne permettent pas aux paramètres de type d'avoir des paramètres. En Java, si T
est une variable de type, vous pouvez écrire T
et List<T>
, mais pas T<String>
. Les types de type supérieur suppriment cette restriction, de sorte que vous puissiez avoir quelque chose comme ça (pas complètement pensé):
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) {
// ...
}
}
Et en abordant ce bit en particulier:
(Je pense) J'obtiens que la place myList |> List.map f
ou myList |> Seq.map f |> Seq.toList
les types supérieurs vous permettent d'écrire simplement myList |> map f
et cela retournera un fichier List
. C'est génial (en supposant que c'est correct), mais cela semble un peu mesquin? (Et cela ne pourrait-il pas être fait simplement en autorisant la surcharge de fonctions?) De Seq
toute façon, je convertis généralement en tout ce que je veux par la suite.
Il existe de nombreux langages qui généralisent l'idée de la map
fonction de cette façon, en la modélisant comme si, au fond, la cartographie concernait des séquences. Cette remarque est dans cet esprit: si vous avez un type qui prend en charge la conversion vers et depuis Seq
, vous obtenez l'opération de carte "gratuitement" en réutilisant Seq.map
.
Dans Haskell, cependant, la Functor
classe est plus générale que cela; il n'est pas lié à la notion de séquences. Vous pouvez implémenter fmap
pour des types qui n'ont pas de bonne correspondance avec les séquences, comme les IO
actions, les combinateurs d'analyseurs, les fonctions, etc.:
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
Le concept de «mapping» n'est pas vraiment lié aux séquences. Il est préférable de comprendre les lois des foncteurs:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
Très informellement:
- La première loi dit que le mappage avec une fonction d'identité / noop revient à ne rien faire.
- La deuxième loi dit que tout résultat que vous pouvez produire en mappant deux fois, vous pouvez également le produire en mappant une fois.
C'est pourquoi vous souhaitez fmap
conserver le type, car dès que vous obtenez des map
opérations qui produisent un type de résultat différent, il devient beaucoup plus difficile de faire des garanties comme celle-ci.