Je fais du développement en F # depuis un moment et j'aime ça. Cependant, un mot à la mode dont je sais qu'il n'existe pas en F # est celui des types de type supérieur. J'ai lu de la documentation sur les types plus élevés et je pense comprendre leur définition. Je ne sais tout simplement pas pourquoi ils sont utiles. Quelqu'un peut-il fournir des exemples de ce que les types de type supérieur facilitent dans Scala ou Haskell, qui nécessitent des solutions de contournement en F #? Aussi pour ces exemples, quelles seraient les solutions de contournement sans types de type supérieur (ou vice-versa en F #)? Peut-être que je suis tellement habitué à le contourner que je ne remarque pas l'absence de cette fonctionnalité.
(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. Encore une fois, je suis peut-être trop habitué à contourner ce problème. Mais y a-t-il un exemple où les types de types plus élevés vous sauvent vraiment en frappes ou en sécurité de type?
IMonad<T>
et la renvoyer par exemple,IEnumerable<int>
ouIObservable<int>
lorsque vous avez terminé? Est-ce juste pour éviter le casting?return
cela fonctionnerait puisque cela appartient vraiment au type monade, pas à une instance particulière, vous ne voudriez donc pas du tout le mettre dans l'IMonad
interface.bind
akaSelectMany
etc. Ce qui signifie que quelqu'un pourrait utiliser l'API pourbind
unIObservable
à unIEnumerable
et supposer que cela fonctionnerait, ce qui ouais ouais si c'est le cas et il n'y a aucun moyen de contourner cela. Mais pas sûr à 100% qu'il n'y a pas moyen de contourner cela.Réponses:
Donc, le type d'un type est son type simple. Par exemple,
Int
a kind,*
ce qui signifie qu'il s'agit d'un type de base et peut être instancié par des valeurs. Par une définition vague du type de type supérieur (et je ne suis pas sûr de savoir où F # trace la ligne, alors incluons-le simplement) les conteneurs polymorphes sont un excellent exemple d'un type de type supérieur.Le constructeur de type
List
a kind* -> *
ce qui signifie qu'il doit recevoir un type concret pour aboutir à un type concret:List Int
peut avoir des habitants comme[1,2,3]
maisList
lui-même ne le peut pas.Je vais supposer que les avantages des conteneurs polymorphes sont évidents, mais il
* -> *
existe des types de types plus utiles que les conteneurs. Par exemple, les relationsou analyseurs
les deux ont aussi du genre
* -> *
.Nous pouvons cependant aller plus loin dans Haskell en ayant des types avec des types d'ordre encore plus élevé. Par exemple, nous pourrions rechercher un type avec kind
(* -> *) -> *
. Un exemple simple de ceci pourrait être celuiShape
qui essaie de remplir un conteneur de type* -> *
.Ceci est utile pour caractériser les
Traversable
s dans Haskell, par exemple, car ils peuvent toujours être divisés en leur forme et leur contenu.Comme autre exemple, considérons un arbre paramétré sur le type de branche dont il dispose. Par exemple, un arbre normal pourrait être
Mais nous pouvons voir que le type de branche contient a
Pair
deTree a
s et nous pouvons donc extraire ce morceau du type de manière paramétriqueCe
TreeG
constructeur de type a kind(* -> *) -> * -> *
. Nous pouvons l'utiliser pour créer d'autres variantes intéressantes comme unRoseTree
Ou pathologiques comme un
MaybeTree
Ou un
TreeTree
Un autre endroit où cela apparaît est dans les "algèbres de foncteurs". Si nous supprimons quelques couches d'abstrait, cela pourrait être mieux considéré comme un pli, tel que
sum :: [Int] -> Int
. Les algèbres sont paramétrées sur le foncteur et le porteur . Le foncteur a du genre* -> *
et le genre de transporteur*
si tout à faita du genre
(* -> *) -> * -> *
.Alg
utile en raison de sa relation avec les types de données et les schémas de récursivité construits sur eux.Enfin, bien qu'ils soient théoriquement possibles, je n'ai jamais vu de constructeur de type encore plus élevé. Nous voyons parfois des fonctions de ce type comme
mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b
, mais je pense que vous devrez fouiller dans le prologue de type ou la littérature typée de manière dépendante pour voir ce niveau de complexité dans les types.la source
TreeTree
est juste pathologique, mais plus concrètement, cela signifie que vous avez deux types d'arbres différents entrelacés l'un de l'autre - pousser cette idée un peu plus loin peut vous apporter des notions très puissantes de sécurité de type telles que le rouge / arbres noirs et le type FingerTree équilibré statiquement.Considérez la
Functor
classe de type dans Haskell, oùf
est une variable de type de type supérieur:Ce que dit cette signature de type, c'est que fmap modifie le paramètre de type d'un
f
dea
àb
, mais laissef
tel quel. Donc, si vous utilisezfmap
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: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éthodeFunnyList<A> implements Functor<A>
dont lamap
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 unFunctor
. De plus, lorsque vous utilisez lamap
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:map
méthode renvoie toujours la mêmeFunctor
sous-classe que le récepteur.Functor
méthode sur le résultat demap
.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:Cela aide à interdire aux implémenteurs de ces interfaces plus étroites de renvoyer le mauvais type de
Functor
de lamap
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 deMonad<B>
dans l'interface suivante: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:
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:Le problème ici est que cela ne se limite pas
FB
à avoir le mêmeF
queFA
- de sorte que lorsque vous déclarez un typeList<A> implements Functor<List<A>, A>
, lamap
méthode peut toujours renvoyer unNotAList<B> implements Functor<NotAList<B>, B>
.Dernier essai, en Java, en utilisant des types bruts (conteneurs non paramétrés):
Ici,
F
seront instanciés vers des types non paramétrés comme justList
ouMap
. Cela garantit que aFunctorStrategy<List>
ne peut renvoyer qu'unList
- 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 écrireT
etList<T>
, mais pasT<String>
. Les types de type supérieur suppriment cette restriction, de sorte que vous puissiez avoir quelque chose comme ça (pas complètement pensé):Et en abordant ce bit en particulier:
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 depuisSeq
, vous obtenez l'opération de carte "gratuitement" en réutilisantSeq.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émenterfmap
pour des types qui n'ont pas de bonne correspondance avec les séquences, comme lesIO
actions, les combinateurs d'analyseurs, les fonctions, etc.:Le concept de «mapping» n'est pas vraiment lié aux séquences. Il est préférable de comprendre les lois des foncteurs:
Très informellement:
C'est pourquoi vous souhaitez
fmap
conserver le type, car dès que vous obtenez desmap
opérations qui produisent un type de résultat différent, il devient beaucoup plus difficile de faire des garanties comme celle-ci.la source
fmap
onFunction a
alors qu'il a déjà une.
opération? Je comprends pourquoi.
cela a du sens d'être la définition de l'opérationfmap
, mais je n'arrive tout simplement pas là où vous auriez jamais besoin d'utiliser à lafmap
place.
. Peut-être que si vous pouviez donner un exemple où cela serait utile, cela m'aiderait à comprendre.double
d'un foncteur, oùdouble [1, 2, 3]
donne[2, 4, 6]
etdouble sin
donne un fn qui est le double du péché. Je peux voir où si vous commencez à penser dans cet état d'esprit, lorsque vous exécutez une carte sur un tableau, vous attendez un tableau en retour, pas seulement une séquence, car, eh bien, nous travaillons sur des tableaux ici.Functor
et de laisser le client de la bibliothèque le choisir. La réponse de J. Abrahamson fournit un exemple: les plis récursifs peuvent être généralisés en utilisant des foncteurs. Un autre exemple est celui des monades gratuites; vous pouvez les considérer comme une sorte de bibliothèque d'implémentation d'interpréteur générique, où le client fournit le "jeu d'instructions" comme arbitraireFunctor
.Functor
ou unSemiGroup
. Où les vrais programmes utilisent-ils le plus cette fonctionnalité de langage?Je ne veux pas répéter les informations dans d'excellentes réponses déjà ici, mais il y a un point clé que j'aimerais ajouter.
Vous n'avez généralement pas besoin de types de type supérieur pour implémenter une monade ou un foncteur particulier (ou un foncteur applicatif, ou une flèche, ou ...). Mais faire cela, c'est manquer l'essentiel.
En général, j'ai trouvé que lorsque les gens ne voient pas l'utilité des foncteurs / monades / quoi que ce soit, c'est souvent parce qu'ils pensent à ces choses une à la fois . Les opérations Functor / monad / etc n'ajoutent vraiment rien à une seule instance (au lieu d'appeler bind, fmap, etc., je pourrais simplement appeler les opérations que j'ai utilisées pour implémenter bind, fmap, etc.). Ce que vous voulez vraiment pour ces abstractions, c'est que vous puissiez avoir du code qui fonctionne de manière générique avec n'importe quel foncteur / monad / etc.
Dans un contexte où ce code générique est largement utilisé, cela signifie que chaque fois que vous écrivez une nouvelle instance de monade, votre type accède immédiatement à un grand nombre d'opérations utiles qui ont déjà été écrites pour vous . C'est le point de voir des monades (et des foncteurs, et ...) partout; non pas pour que je puisse utiliser
bind
plutôt queconcat
etmap
pour implémentermyFunkyListOperation
(ce qui ne me rapporte rien en soi), mais plutôt pour que lorsque j'en ai besoinmyFunkyParserOperation
et quemyFunkyIOOperation
je puisse réutiliser le code que j'ai vu à l'origine en termes de listes car il est en fait monad-générique .Mais pour faire abstraction d'un type paramétré comme une monade avec sécurité de type , vous avez besoin de types de type supérieur (comme expliqué dans d'autres réponses ici).
la source
Pour une perspective plus spécifique à .NET, j'ai écrit un article de blog à ce sujet il y a quelque temps. Le point crucial est que, avec les types de type supérieur, vous pouvez potentiellement réutiliser les mêmes blocs LINQ entre
IEnumerables
etIObservables
, mais sans types de type supérieur, c'est impossible.Le plus proche que vous pourriez obtenir (j'ai compris après avoir publié le blog) est de faire votre propre
IEnumerable<T>
etIObservable<T>
et les deux étendues d'unIMonad<T>
. Cela vous permettrait de réutiliser vos blocs LINQ s'ils sont indiquésIMonad<T>
, mais ce n'est plus du type sécurisé car cela vous permet de mélanger et de faire correspondreIObservables
etIEnumerables
dans le même bloc, ce qui, même si cela peut sembler intriguant d'activer cela, vous simplement obtenir un comportement indéfini.J'ai écrit un article plus tard sur la façon dont Haskell rend cela facile. (Un no-op, vraiment - restreindre un bloc à un certain type de monade nécessite du code; l'activation de la réutilisation est la valeur par défaut).
la source
IObservables
dans le code de production.IObservable
, et vous utilisez des événements dans le chapitre WinForms de votre propre livre.L'exemple le plus utilisé de polymorphisme de type supérieur dans Haskell est l'
Monad
interface.Functor
etApplicative
sont de nature supérieure de la même manière, donc je vais montrerFunctor
afin de montrer quelque chose de concis.Maintenant, examinez cette définition, en regardant comment la variable de type
f
est utilisée. Vous verrez quef
cela ne peut pas signifier un type qui a de la valeur. Vous pouvez identifier les valeurs dans cette signature de type car ce sont des arguments et des résultats d'une fonction. Donc, les variables de typea
etb
sont des types qui peuvent avoir des valeurs. Il en va de même pour les expressions de typef a
etf b
. Mais pasf
lui-même.f
est un exemple de variable de type supérieur. Étant donné que*
c'est le genre de types qui peuvent avoir des valeurs,f
doit avoir le genre* -> *
. Autrement dit, il faut un type qui peut avoir des valeurs, car nous savons d'après un examen précédent quea
etb
doit avoir des valeurs. Et nous savons aussi quef a
etf b
doit avoir des valeurs, il renvoie donc un type qui doit avoir des valeurs.Cela rend le
f
utilisé dans la définition d'Functor
une variable de type supérieur.Les interfaces
Applicative
etMonad
ajoutent plus, mais elles sont compatibles. Cela signifie qu'ils fonctionnent également sur des variables de type avec kind* -> *
.Travailler sur des types de type supérieur introduit un niveau supplémentaire d'abstraction - vous n'êtes pas limité à la création d'abstractions sur les types de base. Vous pouvez également créer des abstractions sur des types qui modifient d'autres types.
la source