Types et éléments de conteneur associés

Je pense que j'ai peut-être demandé ceci sur Haskell-Cafe à un moment donné, mais damné si je peux trouver la réponse maintenant... Donc je le demande encore ici, alors j'espère qu'à l'avenir je pourrai trouver la réponse!

Haskell est Fantastique pour traiter le polymorphisme paramétrique. Mais le problème est que tout n'est pas paramétrique. À titre d'exemple trivial, supposons que nous voulions extraire le premier élément de données d'un conteneur. Pour un type paramétrique, c'est trivial:

class HasFirst c where
  first :: c x -> Maybe x

instance HasFirst [] where
  first []    = Nothing
  first (x:_) = Just x

Maintenant essayez d'écrire une instance pour ByteString. Vous ne pouvez pas. son type ne mentionne pas le type d'élément. Vous ne pouvez pas non plus écrire une instance pour Set, car elle nécessite une contrainte Ord - mais la tête de classe ne mentionne pas le type d'élément, vous ne pouvez donc pas le contraindre.

Les types associés fournissent un moyen efficace de résoudre complètement ces problèmes:

class HasFirst c where
  type Element c :: *
  first :: c -> Maybe (Element c)

instance HasFirst [x] where
  type Element [x] = x
  first []    = Nothing
  first (x:_) = Just x

instance HasFirst ByteString where
  type Element ByteString = Word8
  first b = b ! 0

instance Ord x => HasFirst (Set x) where
  type Element (Set x) = x
  first s = findMin s

, Nous avons maintenant un nouveau problème, cependant. Pensez à essayer de" réparer " Functor pour que cela fonctionne pour tous les types de conteneurs:

class Functor f where
  type Element f :: *
  fmap :: (Functor f2) => (Element f -> Element f2) -> f -> f2

Ce ne fonctionne pas du tout. Il est dit que si nous avons une fonction du type d'élément de f pour le type d'élément de f2, alors on peut transformer un f en f2. So far So good. Cependant, il n'y a apparemment aucun moyen d'exiger que f et f2soient le même type de conteneur!

Selon la définition Functor existante, nous avons

fmap :: (x -> y) -> [x] -> [y]
fmap :: (x -> y) -> Seq x -> Seq y
fmap :: (x -> y) -> IO x -> IO y

, Mais nous n' pas ont fmap :: (x -> y) -> IO x -> [y]. C'est tout à fait impossible. Mais la définition de classe ci-dessus permet il.

Est-ce que quelqu'un sait comment expliquer au système de type ce que signifiait réellement?

Modifier

Ce qui précède fonctionne en définissant un moyen de calculer un type d'élément à partir d'un type de conteneur. Ce qui se passe si vous essayez de le faire dans l'autre sens? Définir une fonction pour calculer un type de conteneur à partir d'un type d'élément? Le fait de faire sortir le plus facile?

21
demandé sur MathematicalOrchid 2012-01-26 14:09:22

2 réponses

Eh bien, le problème est que ce n'est pas clair ce que le Functor révisé est censé signifier. Par exemple, considérez ByteString. Un ByteString ne peut être mappé qu'en remplaçant chaque élément Word8 par un élément du même type. Mais {[5] } est pourparamétrique structures mappables. Il y a vraiment deux notions contradictoires de cartographie ici:

  • mapping rigide (c'est-à-dire transformer chaque élément d'une structure sans changer son type)
  • cartographie paramétrique (c'est-à-dire transformer chaque élément d'une structure à n'importe quel type)

Donc, dans ce cas, vous ne pouvez pas expliquer au système de type ce que vous vouliez dire, car cela n'a pas beaucoup de sens. Vous pouvez, cependant, changer ce que vous voulez dire:)

La cartographie rigide est facile à exprimer avec les familles de types:

class RigidMap f where
  type Element f :: *
  rigidMap :: (Element f -> Element f) -> f -> f

En ce qui concerne la cartographie paramétrique, il y a plusieurs façons de le faire. Le moyen le plus simple serait de conserver le courant Functor tel quel. Ensemble, ces classes couvrent des structures comme ByteString, [], Seq, et ainsi de suite. Cependant, ils tombent tous sur Set et Map, à cause de la contrainte Ord sur les valeurs. Heureusement, l'extension constraint kinds entrant dans GHC 7.4 nous permet de résoudre ce problème:

class RFunctor f where
  type Element f a :: Constraint
  type Element f a = ()  -- default empty constraint
  fmap :: (Element f a, Element f b) => (a -> b) -> f a -> f b

Ici, nous disons que chaque instance devrait avoir une contrainte typeclass associée . Par exemple, L'instance Set aura Element Set a = Ord a, pour indiquer que Sets ne peut être construit que si une instance Ord est disponible pour le type. Tout ce qui peut apparaître sur la main gauche de => est acceptée. Nous pouvons définir nos instances précédentes exactement comme elles étaient, mais nous pouvons aussi faire Set et Map:

instance RFunctor Set where
  type Element Set a = Ord a
  fmap = Set.map

instance RFunctor Map where
  type Element Map a = Ord a
  fmap = Map.map

Cependant, il est assez ennuyeux d'avoir à utiliser deux interfaces distinctes pour le mappage rigide et le mappage paramétrique restreint. En fait, ce dernier n'est-il pas une généralisation du premier? Considérez la différence entre Set, qui ne peut contenir que des instances de Ord, et ByteString, qui ne peut contenir que des Word8s. Nous pouvons sûrement exprimer cela comme juste une autre contrainte?

Nous appliquons la même astuce à HasFirst (c'est-à-dire donner des instances pour toute la structure et utiliser une famille de types pour exposer le type d'élément), et introduire une nouvelle famille de contraintes associée:

class Mappable f where
  type Element f :: *
  type Result f a r :: Constraint
  map :: (Result f a r) => (Element f -> a) -> f -> r

L'idée ici est que Result f a r exprime les contraintes dont il a besoin sur le type de valeur (comme Ord a), et aussi contraint le type de conteneur résultant mais il doit le faire; vraisemblablement, pour s'assurer qu'il a le type d'un même type de conteneur de a s. Pour exemple, Result [a] b r sera sans doute besoin que r est [b], et Result ByteString b r exigera que les b est Word8, et r est ByteString.

Les familles de Type nous donnent déjà ce dont nous avons besoin pour exprimer "est" ici: une contrainte d'égalité de type. Nous pouvons dire (a ~ b) => ... pour exiger que a et b soient du même type. Nous pouvons, bien sûr, l'utiliser dans les définitions de famille de contraintes. Donc, nous avons tout ce dont nous avons besoin; sur les instances:

instance Mappable [a] where
  type Element [a] = a
  type Result [a] b r = r ~ [b]
  -- The type in this case works out as:
  --   map :: (r ~ [b]) => (a -> b) -> [a] -> r
  -- which simplifies to:
  --   map :: (a -> b) -> [a] -> [b]
  map = Prelude.map

instance Mappable ByteString where
  type Element ByteString = Word8
  type Result ByteString a r = (a ~ Word8, r ~ ByteString)
  -- The type is:
  --   map :: (b ~ Word8, r ~ ByteString) => (Word8 -> b) -> ByteString -> r
  -- which simplifies to:
  --   map :: (Word8 -> Word8) -> ByteString -> ByteString
  map = ByteString.map

instance (Ord a) => Mappable (Set a) where
  type Element (Set a) = a
  type Result (Set a) b r = (Ord b, r ~ Set b)
  -- The type is:
  --   map :: (Ord a, Ord b, r ~ Set b) => (a -> b) -> Set a -> r
  -- (note the (Ord a) constraint from the instance head)
  -- which simplifies to:
  --   map :: (Ord a, Ord b) => (a -> b) -> Set a -> Set b
  map = Set.map

Parfaite! Nous pouvons définir des instances pour tout type de conteneur que nous voulons, rigide, paramétrique ou paramétrique-mais-restreint, et les types fonctionnent parfaitement.

Disclaimer: Je n'ai pas encore essayé GHC 7.4, donc je ne sais pas si tout cela compile ou fonctionne, mais je pense que les idées de base sont saines.

22
répondu ehird 2012-01-26 10:57:16

Vous Voulez types de contraintes , disponibles dans ghc 7.4.

6
répondu augustss 2012-01-26 10:30:42