Désolé, je ne connais pas vraiment mes maths, donc je suis curieux de savoir comment prononcer les fonctions dans la classe de types Applicative
Connaître vos maths, ou non, est largement hors de propos ici, je pense. Comme vous le savez probablement, Haskell emprunte quelques bouts de terminologie à divers domaines des mathématiques abstraites, notamment la théorie des catégories , d'où nous obtenons des foncteurs et des monades. L'utilisation de ces termes dans Haskell diverge quelque peu des définitions mathématiques formelles, mais ils sont généralement assez proches pour être de toute façon de bons termes descriptifs.
La Applicative
classe de types se situe quelque part entre Functor
et Monad
, on s'attend donc à ce qu'elle ait une base mathématique similaire. La documentation du Control.Applicative
module commence par:
Ce module décrit une structure intermédiaire entre un foncteur et une monade: il fournit des expressions pures et du séquençage, mais pas de liaison. (Techniquement, un puissant foncteur monoïdal laxiste.)
Hmm.
class (Functor f) => StrongLaxMonoidalFunctor f where
. . .
Pas aussi accrocheur que Monad
je pense.
Ce à quoi tout cela se résume fondamentalement, c'est que Applicative
cela ne correspond à aucun concept particulièrement intéressant mathématiquement, donc il n'y a pas de termes prêts à l'emploi qui traînent qui capturent la façon dont il est utilisé dans Haskell. Alors, mettez les calculs de côté pour le moment.
Si nous voulons savoir comment appeler, (<*>)
il peut être utile de savoir ce que cela signifie fondamentalement.
Alors , quel est avec Applicative
, de toute façon, et pourquoi ne nous l' appelons ça?
Ce qui Applicative
revient en pratique est un moyen de soulever des fonctions arbitraires en a Functor
. Considérez la combinaison de Maybe
(sans doute le plus simple non trivial Functor
) et Bool
(de même le type de données non trivial le plus simple).
maybeNot :: Maybe Bool -> Maybe Bool
maybeNot = fmap not
La fonction fmap
nous permet de passer not
du travail Bool
au travail Maybe Bool
. Mais que faire si nous voulons soulever (&&)
?
maybeAnd' :: Maybe Bool -> Maybe (Bool -> Bool)
maybeAnd' = fmap (&&)
Eh bien, ce n'est pas ce que nous voulons du tout ! En fait, c'est à peu près inutile. Nous pouvons essayer d'être intelligents et Bool
en introduire un autre Maybe
par l'arrière ...
maybeAnd'' :: Maybe Bool -> Bool -> Maybe Bool
maybeAnd'' x y = fmap ($ y) (fmap (&&) x)
... mais ce n'est pas bon. D'une part, c'est faux. Pour autre chose, c'est moche . Nous pourrions continuer d'essayer, mais il s'avère qu'il n'y a aucun moyen de lever une fonction d'arguments multiples pour travailler sur un arbitraireFunctor
. Énervant!
D'un autre côté, nous pourrions le faire facilement si nous Maybe
utilisions l' Monad
instance de:
maybeAnd :: Maybe Bool -> Maybe Bool -> Maybe Bool
maybeAnd x y = do x' <- x
y' <- y
return (x' && y')
Maintenant, c'est beaucoup de tracas juste pour traduire une fonction simple - ce qui est la raison pour laquelle Control.Monad
fournit une fonction pour le faire automatiquement, liftM2
. Le 2 dans son nom fait référence au fait qu'il fonctionne sur des fonctions d'exactement deux arguments; des fonctions similaires existent pour les fonctions d'argument 3, 4 et 5. Ces fonctions sont meilleures , mais pas parfaites, et spécifier le nombre d'arguments est laid et maladroit.
Ce qui nous amène à l' article qui a introduit la classe de type Applicatif . Les auteurs y font essentiellement deux observations:
- Lever des fonctions multi-arguments dans un
Functor
est une chose très naturelle à faire
- Cela ne nécessite pas les capacités complètes d'un
Monad
L'application de fonction normale est écrite par une simple juxtaposition de termes, donc pour rendre «l'application soulevée» aussi simple et naturelle que possible, l'article présente des opérateurs infixes pour remplacer l'application, soulevés dans leFunctor
, et une classe de type pour fournir ce qui est nécessaire pour cela .
Tout cela nous amène au point suivant: (<*>)
représente simplement une application de fonction - alors pourquoi la prononcer différemment de celle de l '«opérateur de juxtaposition» d'espaces blancs?
Mais si ce n'est pas très satisfaisant, on peut observer que le Control.Monad
module fournit également une fonction qui fait la même chose pour les monades:
ap :: (Monad m) => m (a -> b) -> m a -> m b
Où ap
est, bien sûr, l'abréviation de «appliquer». Puisque tout Monad
peut être Applicative
, et ap
n'a besoin que du sous-ensemble de fonctionnalités présentes dans ce dernier, nous pouvons peut-être dire que si ce (<*>)
n'était pas un opérateur, il devrait être appelé ap
.
On peut aussi aborder les choses dans l'autre sens. L' Functor
opération de levage est appelée fmap
car c'est une généralisation de l' map
opération sur les listes. Quel genre de fonction sur les listes fonctionnerait-il (<*>)
? Il y a ce qui ap
fait sur les listes, bien sûr, mais ce n'est pas particulièrement utile en soi.
En fait, il existe une interprétation peut-être plus naturelle des listes. Qu'est-ce qui vous vient à l'esprit lorsque vous regardez la signature de type suivante?
listApply :: [a -> b] -> [a] -> [b]
Il y a quelque chose de tellement tentant dans l'idée d'aligner les listes en parallèle, en appliquant chaque fonction de la première à l'élément correspondant de la seconde. Malheureusement pour notre vieil ami Monad
, cette opération simple viole les lois de la monade si les listes sont de longueurs différentes. Mais cela fait une amende Applicative
, auquel cas (<*>)
devient une façon d' enchaîner une version généralisée de zipWith
, alors peut-être pouvons-nous imaginer l'appeler fzipWith
?
Cette idée de fermeture éclair nous amène en fait à boucler la boucle. Rappelez-vous ce truc de maths plus tôt, à propos des foncteurs monoïdaux? Comme son nom l'indique, il s'agit d'un moyen de combiner la structure des monoïdes et des foncteurs, qui sont tous deux des classes de type Haskell familières:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Monoid a where
mempty :: a
mappend :: a -> a -> a
À quoi ressembleraient-ils si vous les mettiez ensemble dans une boîte et la secouiez un peu? De Functor
nous allons garder l'idée d'une structure indépendante de son paramètre de type , et de Monoid
nous garderons la forme globale des fonctions:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ?
mfAppend :: f ? -> f ? -> f ?
Nous ne voulons pas supposer qu'il existe un moyen de créer un vraiment "vide" Functor
, et nous ne pouvons pas évoquer une valeur d'un type arbitraire, nous allons donc corriger le type de mfEmpty
as f ()
.
Nous ne voulons pas non plus forcer mfAppend
le besoin d'un paramètre de type cohérent, alors maintenant nous avons ceci:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f ?
À quoi sert le type de résultat mfAppend
? Nous avons deux types arbitraires dont nous ne savons rien, donc nous n'avons pas beaucoup d'options. Le plus judicieux est de garder les deux:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f (a, b)
À quel point mfAppend
est maintenant clairement une version généralisée de zip
on lists, et nous pouvons reconstruire Applicative
facilement:
mfPure x = fmap (\() -> x) mfEmpty
mfApply f x = fmap (\(f, x) -> f x) (mfAppend f x)
Cela nous montre également qu'il pure
est lié à l'élément d'identité de a Monoid
, donc d'autres bons noms peuvent être tout ce qui suggère une valeur unitaire, une opération nulle ou autre.
C'était long, donc pour résumer:
(<*>)
est juste une application de fonction modifiée, vous pouvez donc soit la lire comme "ap" ou "appliquer", soit l'éliminer entièrement comme vous le feriez pour une application de fonction normale.
(<*>)
généralise aussi grossièrement zipWith
sur les listes, de sorte que vous pouvez le lire en tant que "foncteurs zip avec", de la même manière fmap
que "mapper un foncteur avec".
Le premier est plus proche de l'intention de la Applicative
classe de type - comme son nom l'indique - c'est donc ce que je recommande.
En fait, j'encourage l'utilisation libérale, et la non-prononciation, de tous les opérateurs d'application levés :
(<$>)
, qui lève une fonction à un seul argument en un Functor
(<*>)
, qui enchaîne une fonction multi-arguments via un Applicative
(=<<)
, qui lie une fonction qui entre un Monad
sur un calcul existant
Tous les trois sont, au fond, juste une application de fonction régulière, un peu épicée.