Envisager l' Functor
type de classe en Haskell, où f
est supérieur kinded type de variable:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Ce que ce type de signature est dit que fmap modifie le paramètre de type d'un f
de a
de b
, mais les feuilles f
seulement. Donc, si vous utilisez fmap
sur une liste, vous obtenez une liste, si vous l'utilisez sur un analyseur de vous obtenir un analyseur, et ainsi de suite. Et ce sont statiques, au moment de la compilation de garanties.
Je ne sais pas F#, mais considérons ce qui se passe si nous essayons d'exprimer l' Functor
d'abstraction dans un langage comme Java ou C#, l'héritage et les génériques, mais pas plus haut-kinded génériques. 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é à retourner en toute classe qui implémente Functor
. Quelqu'un pourrait-il écrire un FunnyList<A> implements Functor<A>
dont map
méthode renvoie un autre type de collection, ou encore autre chose qui n'est pas une collection à tous, mais encore une Functor
. Aussi, chaque fois que vous utilisez l' map
méthode, vous avez abattu le résultat pour le type que vous êtes réellement en attendre.
Il existe d'autres, plus compliquées façons que vous pouvez essayer, mais aucun d'eux ne fonctionne vraiment. Par exemple, vous pourriez essayer d'augmenter l'essayer d'abord par la définition de sous-types de Functor
qui limitent 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 permet d'interdire la mise en œuvre de ces plus étroite des interfaces de retourner le mauvais type d' Functor
de la map
méthode, mais depuis il n'y a pas de limite au nombre Functor
des implémentations vous pouvez avoir, il n'y a pas de limite au nombre plus restreint d'interfaces dont vous aurez besoin.
(EDIT: Et notez que cela ne fonctionne que parce qu' Functor<B>
apparaît comme le type de résultat, et ainsi l'enfant interfaces peuvent le réduire. Donc, autant que je sache, nous ne pouvons pas étroit les deux utilisations Monad<B>
dans l'interface suivante:
interface Monad<A> {
<B> Monad<B> bind(Function<A, Monad<B>> f);
}
En Haskell, avec plus rang des variables de type, c'est - bind :: Monad m => m a -> (a -> m b) -> m b
.)
Encore un autre essai est d'utiliser récursive génériques et tenter de l'interface de restreindre le type de résultat du sous-type du sous-type lui-même. Jouet exemple:
/**
* 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 sans plus-kinded polymorphisme, ce genre de technique (ce qui est plutôt arcanes de votre run-of-the-mill OOP développeur) ne peut pas toujours exprimer l'souhaité Functor
contrainte:
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
Le problème ici c'est que cela ne limite pas l' FB
d'avoir le même F
comme FA
-de sorte que, lorsque vous déclarez un type List<A> implements Functor<List<A>, A>
, map
méthode peut toujours retourner quelque chose qui n'est pas un List
.
Finale essayer, à Java, à l'aide de matières types (unparametrized conteneurs):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Ici, F
obtiendrez instancié à unparametrized types comme juste List
ou Map
. Cela garantit que l' FunctorStrategy<List>
ne peut retourner un List
-mais vous avez abandonné l'utilisation de variables de type à suivre le type d'élément de la liste.
Le cœur du problème ici, c'est que les langues comme Java et C# ne permettent pas de paramètres de type à avoir des paramètres. En Java, si T
est une variable de type, vous pouvez écrire T
et List<T>
, mais pas T<String>
. Plus-kinded types de supprimer cette restriction, de sorte que vous pourriez avoir quelque chose comme ceci:
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) {
// ...
}
}
Et de relever ce morceau en particulier:
(Je pense) je suis qu'au lieu de myList |> List.map f
ou myList |> Seq.map f |> Seq.toList
plus élevé kinded types vous permettent de simplement écrire myList |> map f
, et il va retourner un List
. C'est génial (en supposant que c'est correct), mais il semble que ce genre de petits? (Et ne pourrait-il pas être fait simplement en permettant à la fonction de surcharge?) J'ai l'habitude de convertir Seq
de toute façon et puis je peux convertir à ce que je veux par la suite.
Il y a beaucoup de langues que de généraliser l'idée de l' map
fonction de cette manière, par la modélisation, comme si, au fond, la cartographie est à propos de séquences. Cette remarque de la vôtre est dans cet esprit: si vous avez un type qui prend en charge la conversion vers et depuis Seq
,, vous obtenez la carte de l'opération "gratuitement" par la réutilisation de l' Seq.map
.
En Haskell, cependant, l' Functor
la classe est plus général que cela; il n'est pas lié à la notion de séquences. Vous pouvez implémenter fmap
pour les types qui n'ont pas une bonne cartographie des séquences, comme IO
d'actions, analyseur combinators, des 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 "cartographie" n'est pas vraiment lié à des séquences. Il est préférable de comprendre le foncteur lois:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
De façon très informelle:
- La première loi dit que la cartographie avec une identité/noop la fonction est la même que de ne rien faire.
- La deuxième loi dit que tout résultat que vous pouvez produire de la cartographie à deux reprises, vous pouvez également produire de la cartographie des fois.
C'est pourquoi vous souhaitez fmap
afin de préserver la type-parce que dès que vous arrivez map
des opérations qui produisent un résultat différent type, il devient beaucoup, beaucoup plus difficile de donner des garanties de ce genre.