2016-10-21 2 views
1

J'ai le type ci-dessous:Séparation de valeurs de type personnalisé à l'aide de foncteurs?

data Animals a = Cow a a | Dog a a deriving (Show, Eq, Ord) 

J'ai essayer d'appliquer différentes fonctions à chacune des valeurs « a » associé avec les instances des animaux.

E.g.

myCow = "Big" "White" 

function1 :: Animals a -> Animals a 
function1 cow = --do something with "Big", then something else with "White" 

Cela ne fonctionnera pas avec ma définition foncteur:

instance Functor Animals where 
    fmap f (Cow a b) = Cow (f a) (f b) --I want to apply different functions to 'a' and 'b'! 

Comment puis-je définir mon foncteur pour permettre cela?

+4

Vous ne pouvez pas. 'fmap' ne prend qu'une seule fonction comme un argument, et il n'y a aucun moyen pour cette fonction de faire une distinction entre les deux valeurs enveloppées par' Cow' et 'Dog'. Si vous redéfinissez votre type en tant que «Bifonctor», vous pouvez appliquer des fonctions séparées via 'bimap'. – chepner

+0

@chepner Compris, merci! –

Répondre

3

Le type de fmap pour votre type de données serait (a -> b) -> Animals a -> Animals b. Cela contraint très étroitement les implémentations possibles de fmap, tellement que est seulement une implémentation possible qui obéit aux lois de foncteur (fmap id == id, fmap (f . g) == fmap f . fmap g).

instance Functor Animals where 
    fmap f (Cow a b) = ??? 
    fmap f (Dog a b) = ??? 

D'abord, vous ne savez rien sur la fonction f autre qu'elle peut être appliquée à chacune des valeurs enveloppées par les constructeurs de données, de sorte que ce que nous pouvons faire avec tout.

Deuxièmement, nous savons que la valeur de retour doit être de type Animals b, et que nous ne pouvons pas passer de Cow à Dog ou vice versa (sinon, fmap id == id ne pas tenir). De là, nous savons que l'instance doit ressembler à

instance Functor Animals where 
    fmap f (Cow a b) = Cow (f a) (f b) 
    fmap f (Dog a b) = Dog (f a) (f b) 

Il n'y a pas de place dans la signature de type de spécifier une deuxième fonction g, et il n'y a aucun moyen pour f de savoir qui « la moitié » de la valeur Animals il On lui applique pour simuler deux fonctions différentes.


Si vous redéfinissez à la place de votre type avec

data Animals' a b = Cow a b | Dog a b 
type Animal a = Animals' a a 

vous pouvez définir une instance de Bifunctor pour Animals', qui définit une fonction Bimap que peut appliquer une fonction différente à chaque moitié.

instance Bifunctor Animals' where 
    bimap f g (Cow a b) = Cow (f a) (g b) 
    bimap f g (Dog a b) = Dog (f a) (g b) 

Vous pouvez également obtenir deux fonctions supplémentaires first et second qui appliquent une seule fonction uniquement les premier et second types enveloppés par le constructeur de type. Ils ont implémentations par défaut de first f = bimap f id et second g = bimap id g, donc vous n'avez pas besoin de définir si vous avez défini bimap. (De même, bimap f g = first f . second g par défaut, donc il serait suffisant pour définir la paire first et second au lieu de bimap.)

Assurez-vous de ne jamais utiliser Animals' directement pour vous assurer qu'aucun de vos fonctions acceptera une Chimère comme Cow 3 "c".

+2

"Le seul inconvénient de cette approche est que votre type ne contraint plus les deux valeurs enveloppées par les constructeurs de données à avoir le même type" - il suffit de travailler avec 'type Animals a = Animals aa' –

+0

Vous pouvez même utiliser' newtype Animals a = Animaux (Animals 'aa) ', qui est lui-même un' Functor'. – dfeuer

+0

@dfeuer Vous avez besoin de l'alias de type pour que vous puissiez toujours utiliser 'bimap', non? – chepner

1

Comme chepner démontré avec justesse dans leur réponse, vous avez un trilemme à traiter.Il n'y a aucun moyen d'avoir Animals avec des champs de types nécessairement égaux tout en étant capable de cartographier différentes fonctions sur chaque champ et en profitant des avantages d'une classe de foncteurs bien connue et répandue - vous devez déposer l'un des trois:

  • vous pouvez garder le type original et écrire une instance Functor qui fait la même chose à des champs de façon homogène, ce qui est ce que vous essayez d'éviter ...

  • ... ou changer Animals a-Animals a b , en sacrifiant un de vos invariants pour faire un Bifunctor exemple possible, ce qui est la solution de chepner ...

  • ... ou vous contenter d'une fonction de mappage spécifique à Animals, qui n'a pas de généralité, mais au moins implémente exactement ce que vous voulez:

animalMap :: (a -> b) -> (a -> b) -> Animals a -> Animals b 
animalMap f g ani = case ani of 
    Cow x y -> Cow (f x) (g y) 
    Dog x y -> Dog (f x) (g y) 

Sur l'équilibre des choses, j'ai une légère préférence pour la troisième solution, aussi décevante soit-elle.


Le reste de cette réponse est une longue note dans laquelle je vais montrer une façon de faire la troisième solution un peu plus agréable. Je ne suggère pas que vous l'utilisiez réellement - il est presque certainement exagéré pour votre cas d'utilisation - mais c'est une bonne possibilité d'en être conscient.

Comme vous le savez, l'une des grandes choses au sujet des fonctions dans Haskell est qu'ils peuvent être composés:

GHCi> ((2+) . (3*) . (4+)) 1 
17 

Composition nous permet de réfléchir à des fonctions indépendamment des données concrètes qu'ils affectent. Etre capable de composer d'une manière propre et facile est bénéfique à bien des égards, peut-être trop pour être listé ici.

Maintenant, tout autant si vous regardez à nouveau animalMap et d'examiner comment similaire, il est à fmap, vous seriez justifié de penser que chaque paire de fonctions a -> b spécifie un moyen de transformer (via animalMap) un Animals comme une fonction unique indique une façon de transformer une liste ou toute autre valeur Functor (via fmap):

GHCi> let rex = Dog 2 5 
GHCi> let ff = ((2*) . (1+), (3*) . (4+)) 
GHCi> (\(f, g) -> animalMap f g) ff rex 
Dog 6 27 
GHCi> -- Or, equivalently: 
GHCi> uncurry animalMap ff rex 
Dog 6 27 
GHCi> -- A different pair: 
GHCi> let gg = ((1+), subtract 3) 
GHCi> uncurry animalMap gg rex 
Dog 3 2 

cela étant, il serait raisonnable de vouloir composer des paires de fonctions destinées à être utilisées avec animalMap, juste comme régulier les fonctions sont composées. Faire que la manière immédiatement évidente, cependant, est très salissant:

GHCi> uncurry animalMap ((\(h, k) (f, g) -> (h . f, k . g)) gg ff) rex -- yuck 
Dog 7 24 

Vous pouvez, bien sûr, éviter d'écrire que laide lambda explicitement à l'aide pour définir une fonction de composition séparée, analogue à (.) mais spécifiques à votre cas d'utilisation. Cela n'éclaircit cependant pas les choses.

La torsion dans cette histoire est qu'il existe réellement une classe de type standard qui généralise (.) au-delà des fonctions à d'autres choses qui peuvent être composées. Cette classe est appelée Category.Si l'on définit un nouveau type pour les fonctions paires, nous pouvons donner un exemple de Category comme ceci:

import Control.Category 
import Prelude hiding (id, (.)) 

newtype Duof a b = Duof { runDuof :: (a -> b, a -> b) } 

instance Category Duof where 
    id = Duof (id, id) 
    (Duof (h, k)) . (Duof (f, g)) = Duof (h . f, k . g) 

Ensuite, nous pourrions aussi bien redéfinir animalMap en termes de Duof:

animalMap :: Duof a b -> Animals a -> Animals b 
animalMap (Duof (f, g)) ani = case ani of 
    Cow x y -> Cow (f x) (g y) 
    Dog x y -> Dog (f x) (g y) 

Le résultat net est que la composition est beaucoup plus propre:

GHCi> let ff = Duof ((2*) . (1+), (3*) . (4+)) 
GHCi> let gg = Duof ((1+), subtract 3) 
GHCi> animalMap (gg . ff) rex 
Dog 7 24 

Notez que cette façon nouvelle animalMap ressemble beaucoup comme fmap pour Animals, sauf qu'il prend un Duof au lieu d'une fonction. En fait, rien ne nous empêche de définir une nouvelle classe de type appelée DuofFunctor avec une méthode duofmap :: Duof a b -> f a -> f b et de faire Animals une instance de celle-ci - rien, c'est-à-dire, sauf qu'il est inutile de définir une nouvelle classe polyvalente si vous n'en avez besoin que pour un seul exemple. En tout cas, ce DuofFunctor correspondrait exactement à la façon dont vous vouliez écrire l'instance Functor avant que vous réalisiez que c'était impossible.


P.S .: Une remarque sur les conventions de dénomination, sans rapport avec la question elle-même. Habituellement, nous appellerions votre type de données Animal, plutôt que Animals. Même si votre type de données couvre plusieurs espèces d'animaux, les types de données ont presque toujours des noms au singulier, car ces noms sont une description d'une valeur individuelle de ce type (par exemple une vache est un Animal, et un chien aussi) .