96 votes

Quand est-ce que des types plus élevés sont utiles?

Je fais du dev en F#, pendant un certain temps et je l'aime. Cependant, un mot à la mode, je le sais n'existe pas dans F# est supérieur kinded types. J'ai lu le matériau supérieur en kinded types, et je crois que je comprends de leur définition. Je ne sais pas pourquoi ils sont utiles. Quelqu'un peut-il donner quelques exemples de ce que les plus-kinded types de le rendre facile à Scala ou Haskell, qui nécessitent des solutions de contournement en F#? Aussi pour ces exemples, quelles seraient les solutions de contournement sans plus-kinded types (ou vice-versa, F#)? Peut-être que je suis juste l'habitude de travailler autour d'elle que je n'ai pas remarqué l'absence de cette fonctionnalité.

(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. Encore une fois, peut-être que je suis juste trop l'habitude de travailler autour d'elle. Mais est-il un exemple de cas où la plus-kinded types vraiment vous permet d'économiser soit dans les frappes de touches ou dans le type de sécurité?

88voto

J. Abrahamson Points 27606

Ainsi, le genre de type est son type simple. Par exemple, Int a * ce qui signifie que c'est un type de base et peut être instancié par des valeurs. Par certains lâche définition de la plus-kinded type (et je ne suis pas sûr de l'endroit où F# trace la ligne, donc nous allons simplement l'inclure) polymorphe conteneurs sont un excellent exemple de la plus-kinded type.

data List a = Cons a (List a) | Nil

Le constructeur de type List a * -> * ce qui signifie qu'il doit être passé d'un type de béton dans un type de béton: List Int peut avoir des habitants, comme [1,2,3] mais List lui-même ne le peut pas.

Je vais supposer que les avantages de la polymorphes conteneurs sont évidents, mais plus utile genre * -> * il existe des types que juste les conteneurs. Par exemple, les relations

data Rel a = Rel (a -> a -> Bool)

ou analyseurs

data Parser a = Parser (String -> [(a, String)])

les deux ont également des sortes * -> *.


Nous pouvons aller plus loin en Haskell, cependant, par le fait d'avoir des types avec le même ordre supérieur sortes. Par exemple, on pourrait chercher un type avec l'aimable (* -> *) -> *. Un exemple simple de ce qui pourrait être Shape qui tente de remplir un conteneur de type * -> *.

data Shape f = Shape (f ())

[(), (), ()] :: Shape List

C'est utile pour la caractérisation Traversables en Haskell, par exemple, ils peuvent toujours être divisé en leur forme et de leur contenu.

split :: Traversable t => t a -> (Shape t, [a])

Comme autre exemple, considérons un arbre c'est paramétrée sur la nature de la branche. Par exemple, un arbre peut être

data Tree a = Branch (Tree a) a (Tree a) | Leaf

Mais nous pouvons voir que la branche de type contient un Pair de Tree as et on peut donc extraire une pièce du type paramétrique

data TreeG f a = Branch a (f (TreeG f a)) | Leaf

data Pair a = Pair a a
type Tree a = TreeG Pair a

Cette TreeG constructeur de type a type (* -> *) -> * -> *. Nous pouvons l'utiliser pour effectuer d'intéressantes d'autres variantes comme un RoseTree

type RoseTree a = TreeG [] a

rose :: RoseTree Int
rose = Branch 3 [Branch 2 [Leaf, Leaf], Leaf, Branch 4 [Branch 4 []]]

Ou pathologiques comme un MaybeTree

data Empty a = Empty
type MaybeTree a = TreeG Empty a

nothing :: MaybeTree a
nothing = Leaf

just :: a -> MaybeTree a
just a = Branch a Empty

Ou un TreeTree

type TreeTree a = TreeG Tree a

treetree :: TreeTree Int
treetree = Branch 3 (Branch Leaf (Pair Leaf Leaf))

Un autre endroit de cette affiche est dans "les algèbres de foncteurs". Si nous arrêtons de quelques couches d'abstraction cela pourrait être mieux considérée comme un pli, comme sum :: [Int] -> Int. Algèbres sont paramétrées sur le foncteur et le transporteur. Le foncteur a genre * -> * et le transporteur type * donc, tout à fait

data Alg f a = Alg (f a -> a)

a genre (* -> *) -> * -> *. Alg utile en raison de sa relation avec les types de données et les schémas de récursion construit au sommet d'eux.

-- | The "single-layer of an expression" functor has kind `(* -> *)`
data ExpF x = Lit Int
            | Add x x
            | Sub x x
            | Mult x x

-- | The fixed point of a functor has kind `(* -> *) -> *`
data Fix f = Fix (f (Fix f))

type Exp = Fix ExpF

exp :: Exp
exp = Fix (Add (Fix (Lit 3)) (Fix (Lit 4))) -- 3 + 4

fold :: Functor f => Alg f a -> Fix f -> a
fold (Alg phi) (Fix f) = phi (fmap (fold (Alg phi)) f)

Enfin, s'ils sont théoriquement possible, je n'ai jamais voir un même supérieur kinded constructeur de type. 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 aurez à creuser dans le type prolog ou dépendante tapé la littérature de voir que le niveau de complexité dans les types de.

67voto

Luis Casillas Points 11718

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:

  1. La première loi dit que la cartographie avec une identité/noop la fonction est la même que de ne rien faire.
  2. 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.

31voto

Ben Points 22160

Je ne veux pas répéter l'information dans d'excellentes réponses déjà ici, mais il y a un point essentiel que je voudrais ajouter.

Vous n'avez généralement pas besoin de plus-kinded types à mettre en œuvre toute monade, ou functor (ou applicative foncteur, ou les flèches, ou ...). Mais cela est la plupart du temps à côté de l'essentiel.

En général, j'ai constaté que lorsque les gens ne voient pas l'utilité de foncteurs/monades/whatevers, c'est souvent parce qu'ils pensent à ces choses une à la fois. Foncteur/monade/etc opérations vraiment rien ajouter à l'une quelconque instance (au lieu de l'appeler bind, fmap, etc j'ai juste l'appel quel que soit les opérations que j'ai utilisé pour mettre en œuvre lier, fmap, etc). Ce que vous voulez vraiment ces abstractions pour est de sorte que vous pouvez avoir un code qui fonctionne de manière générique avec tout foncteur/monade/etc.

Dans un contexte où un tel code générique est largement utilisé, cela signifie que chaque fois que vous écrivez un nouveau monade exemple votre type gagne immédiatement accès à un grand nombre d'opérations très utiles qui ont déjà été écrit pour vous. C'est le point de voir monades (et foncteurs, et ...) partout; pas de sorte que je peux utiliser bind plutôt que d' concat et map à mettre en oeuvre myFunkyListOperation (dont les gains ne m'a rien en lui-même), mais plutôt de sorte que lorsque je viens à besoin d' myFunkyParserOperation et myFunkyIOOperation Je peux ré-utiliser le code je l'ai d'abord vu en termes de listes, car c'est de l'errance générique.

Mais à l'abstrait à travers un paramétrer le type comme une monade avec le type de la sécurité, vous avez besoin de plus haut kinded types (comme bien expliqué dans d'autres réponses ici).

15voto

Dax Fohl Points 3616

Pour une plus .NET-point de vue spécifique, j'ai écrit un post de blog sur ce dos pendant une. L'essentiel de il est, avec plus de kinded types, vous pourriez potentiellement réutiliser le même LINQ blocs entre IEnumerables et IObservables, mais sans plus-kinded types, ce qui est impossible.

Le plus proche que vous pourriez obtenir (j'ai compris après l'affichage du blog) est de faire votre propre IEnumerable<T> et IObservable<T> et les a étendues à la fois à partir d'un IMonad<T>. Cela vous permettra de réutiliser vos LINQ blocs s'ils sont notées IMonad<T>, mais ensuite il n'est plus typesafe, car il vous permet de mix-and-match IObservables et IEnumerables dans le même bloc, qui, même s'il peut sembler intrigante pour ce faire, vous feriez fondamentalement juste obtenir quelques un comportement indéfini.

J'ai écrit un post plus tard sur la façon dont Haskell permet cela facilement. (Un no-op, vraiment-la limitation d'un bloc à un certain type de monade nécessite le code; afin de permettre la réutilisation est la valeur par défaut).

13voto

Carl Points 10866

Le plus utilisé l'exemple de la plus-kinded type de polymorphisme en Haskell est l' Monad interface. Functor et Applicative plus kinded de la même façon, donc je vais vous montrer Functor afin de montrer quelque chose de concis.

class Functor f where
    fmap :: (a -> b) -> f a -> f b

Maintenant, examinez cette définition, en regardant la façon dont le type de la variable f est utilisé. Vous verrez qu' f peut pas dire à un type qui a de la valeur. Vous pouvez identifier les valeurs de ce type de signature parce qu'ils sont les arguments pour et des résultats de fonctions. Donc, le type des variables a et b sont des types qui peuvent avoir des valeurs. Les expressions de type f a et f b. Mais pas f lui-même. f est un exemple de plus-kinded type de variable. Étant donné qu' * est le genre de types qui peuvent avoir des valeurs, f doit avoir le type * -> *. Qui est, il faut un type qui peut avoir des valeurs, parce que nous savons de l'examen précédent qu' a et b doivent avoir des valeurs. Et nous savons aussi qu' f a et f b doivent avoir des valeurs, elle retourne un type qui doit avoir des valeurs.

Cela rend l' f utilisé dans la définition de l' Functor supérieur kinded type de variable.

L' Applicative et Monad interfaces en ajouter d'autres, mais ils sont compatibles. Cela signifie qu'ils travaillent sur des variables de type avec l'aimable * -> * .

De travail sur l'enseignement supérieur-kinded types introduit un niveau supplémentaire d'abstraction - vous n'êtes pas restreint à la création d'abstractions plus de types de base. Vous pouvez également créer des abstractions plus de types que de modifier d'autres types.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X