59 votes

Impredicative types vs âgé de plaine de sous-typage

Un ami m'a posé une apparence anodins Scala question de la langue, la semaine dernière que je n'ai pas une bonne réponse à la question de savoir si il existe un moyen facile de déclarer une collection de choses appartenant à une communauté de typeclass. Bien sûr, il n'y a pas de première classe de la notion de "typeclass" en Scala, de sorte que nous devons penser en termes de traits de personnalité et le contexte de limite (c'est à dire implicites).

Concrètement, compte tenu de certains traits T[_] représentant un typeclass, et les types d' A, B et C, correspondant implicites dans le champ d'application T[A], T[B] et T[C], nous voulons déclarer quelque chose comme un List[T[a] forAll { type a }], dans laquelle nous pouvons jeter les instances de l' A, B et C avec l'impunité. Bien sûr, cela n'existe pas dans la Scala; une question l'année dernière traite de cette question plus en profondeur.

Le prolongement naturel de la question est "comment Haskell faire?" Eh bien, GHC, en particulier, a un système de type extension appelée impredicative polymorphisme, décrit dans le "Boxy"Types de papier. En bref, compte tenu d'une typeclass T on peut légalement construire une liste [forall a. T a => a]. Donné une déclaration de ce formulaire, le compilateur ne dictionnaire de passage de magie qui nous permet de conserver la typeclass instances correspondant aux types de chaque valeur de la liste au moment de l'exécution.

Chose est, le "dictionnaire de passage de la magie" sonne un peu comme "vtables." Dans un langage orienté objet comme la Scala, le sous-typage est beaucoup plus simple, le mécanisme naturel de la "Boxy" Types d'approche. Si notre A, B et C tous étendre trait T, alors on peut simplement déclarer List[T] et d'être heureux. De même, à mesure que les Kilomètres de notes dans un commentaire ci-dessous, si ils sont tous d'étendre les traits T1, T2 et T3 alors je peux utiliser List[T1 with T2 with T3] comme un équivalent de la impredicative Haskell [forall a. (T1 a, T2 a, T3 a) => a].

Toutefois, le principal, bien connu inconvénient de sous-typage par rapport à typeclasses est le couplage: mon A, B et C types de ont leur T comportement cuit. Imaginons que c'est l'un des principaux dealbreaker, et je ne peux pas utiliser de sous-typage. De sorte que le milieu de terrain de la Scala est proxénètes^H^H^H^H^Himplicit conversions: compte tenu de certains A => T, B => T et C => T dans implicite portée, je peux à nouveau très heureux de remplir un List[T] mon A, B et C valeurs...

... Jusqu'à ce que nous voulons List[T1 with T2 with T3]. À ce point, même si nous avons des conversions implicites A => T1, A => T2 et A => T3, on ne peut pas mettre un A dans la liste. Nous avons pu restructurer nos conversions implicites à fournissons A => T1 with T2 with T3, mais je n'ai jamais vu quelqu'un le faire avant, et il semble être encore une autre forme de couplage.

Ok, donc ma question, enfin, est, je suppose, une combinaison d'un couple de questions qui ont été précédemment demandé: "pourquoi éviter de sous-typage?" et "les avantages de sous-typage sur typeclasses" ... est-il une théorie unificatrice qui dit impredicative polymorphisme et le sous-type de polymorphisme sont une seule et même chose? Sont conversions implicites en quelque sorte le secret de l'amour-l'enfant des deux? Et quelqu'un peut-il articuler un bon modèle pour l'expression de plusieurs limites (comme dans le dernier exemple ci-dessus) à la Scala?

22voto

hammar Points 89293

Vous êtes confus impredicative types avec existentielle types. Impredicative types vous permettent de mettre polymorphes valeurs dans une structure de données, pas arbitraire de béton. En d'autres mots, [forall a. Num a => a] signifie que vous avez une liste où chaque élément fonctionne comme tout type numérique, de sorte que vous ne pouvez pas mettre par exemple Int et Double dans une liste de type [forall a. Num a => a], mais vous pouvez mettre quelque chose comme 0 :: Num a => a . Impredicative types n'est pas ce que vous voulez ici.

Ce que vous voulez, c'est existentiel ( [exists a. Num a => a] (pas de réel Haskell syntaxe), qui dit que chaque élément est un inconnu type numérique. Pour écrire ce en Haskell, cependant, nous avons besoin d'introduire un wrapper de données de type:

data SomeNumber = forall a. Num a => SomeNumber a

Notez le changement de exists de forall. C'est parce que nous sommes en train de décrire le constructeur. On peut mettre n'importe quel type numérique dans l', mais alors le système de type "oublie" de quel type il est. Une fois que nous aurons à revenir (par correspondance), tout ce que nous savons, c'est que c'est un type numérique. Ce qui se passe sous le capot, c'est que l' SomeNumber type contient un champ caché qui stocke le type de la classe dictionary (aka. vtable/implicite), qui est pourquoi nous avons besoin de l'emballage du type.

Maintenant, nous pouvons utiliser le type [SomeNumber] pour une liste arbitraire de chiffres, mais nous avons besoin d'envelopper chaque numéro sur la façon, par exemple, [SomeNumber (3.14 :: Double), SomeNumber (42 :: Int)]. Le bon dictionnaire pour chaque type est recherché et stockée dans le champ masqué automatiquement au point où nous envelopper chaque numéro.

La combinaison existentiel, les types et les classes de type est à certains égards similaire à sous-typage, depuis la principale différence entre le type de classes et d'interfaces avec des classes de type de la vtable de voyages séparément à partir des objets, et existentiel types de paquets objets et les vtables de nouveau ensemble.

Cependant, contrairement aux traditionnels sous-typage, vous n'êtes pas forcé de paire, un à un, donc on peut écrire des choses comme cela qui les paquets d'une vtable avec deux valeurs de même type.

data TwoNumbers = forall a. Num a => TwoNumbers a a

f :: TwoNumbers -> TwoNumbers
f (TwoNumbers x y) = TwoNumbers (x+y) (x*y)

list1 = map f [TwoNumbers (42 :: Int) 7, TwoNumbers (3.14 :: Double) 9]
-- ==> [TwoNumbers (49 :: Int) 294, TwoNumbers (12.14 :: Double) 28.26]

ou même des choses plus. Une fois que nous avons mise en correspondance du modèle sur l'emballage, nous sommes de retour à la terre des classes de type. Bien que nous ne savons pas quel type x et y sont, nous savons qu'ils sont les mêmes, et que nous avons le bon dictionnaire disponible pour effectuer des opérations numériques.

Tout ci-dessus fonctionne de la même façon avec plusieurs type de classes. Le compilateur va tout simplement de générer des champs cachés dans l'emballage de type pour chaque vtable et de les mettre tous dans le champ d'application lorsque nous en correspondance du modèle.

data SomeBoundedNumber = forall a. (Bounded a, Num a) => SBN a

g :: SomeBoundedNumber -> SomeBoundedNumber
g (SBN n) = SBN (maxBound - n)

list2 = map g [SBN (42 :: Int32), SBN (42 :: Int64)]
-- ==> [SBN (2147483605 :: Int32), SBN (9223372036854775765 :: Int64)]

Comme je suis très bien à un débutant quand il s'agit de la Scala, je ne suis pas sûr que je peux vous aider avec la dernière partie de votre question, mais j'espère que cela a au moins éclairci la confusion et de vous donner quelques idées sur la façon de procéder.

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