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) .
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
@chepner Compris, merci! –