Chapitre 10 : Foncteurs applicatifs
Application des foncteurs applicatifs
Le nom foncteur applicatif est particulièrement évocateur compte tenu de ses origines fonctionnelles. Les programmeurs fonctionnels sont connus pour inventer des noms comme mappend ou liftA4, qui semblent naturels en contexte académique mais deviennent aussi clairs qu'un Darth Vader indécis au drive-in dans tout autre cadre.
Quoi qu'il en soit, ce nom révèle la fonctionnalité principale : la capacité d'appliquer des foncteurs entre eux.
Pourquoi une personne rationnelle voudrait-elle une telle fonctionnalité ? Que signifie réellement appliquer un foncteur à un autre ?
Pour répondre, partons d'un scénario commun en programmation fonctionnelle. Supposons deux foncteurs (de même type) et une fonction nécessitant leurs valeurs, comme additionner les contenus de deux Container.
// We can't do this because the numbers are bottled up.
add(Container.of(2), Container.of(3));
// NaN
// Let's use our trusty map
const containerOfAdd2 = map(add, Container.of(2));
// Container(add(2))Nous avons un Container avec une fonction partiellement appliquée (Container(add(2))), et souhaitons appliquer add(2) à la valeur 3 de Container(3). Autrement dit, appliquer un foncteur à un autre.
Nous possédons déjà les outils nécessaires via chain et map :
Container.of(2).chain(two => Container.of(3).map(add(two)));Le problème réside dans l'évaluation séquentielle des monades, où chaque opération doit attendre la précédente. Ces valeurs indépendantes ne devraient pas nécessiter ce séquencement artificiel.
L'idéal serait d'appliquer directement les contenus de foncteurs sans variables intermédiaires superflues.
Bateaux en bouteille

La méthode ap applique le contenu fonctionnel d'un foncteur à la valeur d'un autre. Essayez de le répéter cinq fois rapidement.
Container.of(add(2)).ap(Container.of(3));
// Container(5)
// all together now
Container.of(2).map(add).ap(Container.of(3));
// Container(5)Nous libérons ainsi Container(3) de l'imbrication monadique. Notons que add doit être curryfié pour que cette application partielle fonctionne.
Définition de ap :
Container.prototype.ap = function (otherContainer) {
return otherContainer.map(this.$value);
};Le this.$value étant une fonction, il suffit de map sur l'autre foncteur. Ainsi se définit l'interface :
Un foncteur applicatif est un foncteur pointé doté d'une méthode
ap.
L'interface pointée est cruciale ici, comme le montreront les exemples suivants.
Gardons l'esprit ouvert : ap s'avérera utile. Étudions d'abord une propriété intéressante.
F.of(x).map(f) === F.of(f).ap(F.of(x));Mapper f équivaut à ap un foncteur contenant f. Autrement dit : x.map(f) équivaut à Container(f).ap(x). Cela permet une écriture gauche-droite :
Maybe.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
// Maybe(5)
Task.of(add).ap(Task.of(2)).ap(Task.of(3));
// Task(5)Ce pattern rappelle une structure d'appel de fonction standard. En utilisant of, les valeurs entrent dans l'univers des conteneurs où ap opère, permettant des calculs asynchrones ou conditionnels - comme construire un bateau en bouteille.
Notre exemple avec Task illustre parfaitement l'utilité des foncteurs applicatifs. Approfondissons avec un autre cas.
Motivation par la coordination
Imaginons un site de voyage nécessitant deux appels API distincts : destinations touristiques et événements locaux.
// Http.get :: String -> Task Error HTML
const renderPage = curry((destinations, events) => { /* render page */ });
Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events'));
// Task("<div>some page with dest and events</div>")Les appels Http s'exécutent concurremment, et renderPage est déclenchée quand les deux sont résolus. Contrairement à l'approche monadique séquentielle, ici l'évaluation est parallèle.
La curryfication de renderPage est essentielle. Cette simplicité élégante nous rapproche de la singularité technologique.
Autre exemple :
// $ :: String -> IO DOM
const $ = selector => new IO(() => document.querySelector(selector));
// getVal :: String -> IO String
const getVal = compose(map(prop('value')), $);
// signIn :: String -> String -> Bool -> User
const signIn = curry((username, password, rememberMe) => { /* signing in */ });
IO.of(signIn).ap(getVal('#email')).ap(getVal('#password')).ap(IO.of(false));
// IO({ id: 3, email: 'gg@allin.com' })signIn étant curryfiée à 3 arguments, chaque ap fournit un paramètre. Notons que les deux premiers arguments sont naturellement dans IO, tandis que le dernier nécessite of pour l'alignement typologique.
Mon ami, savez-vous lever ?
Explorons une écriture pointfree. Puisque map équivaut à of/ap, on peut définir des fonctions génériques :
const liftA2 = curry((g, f1, f2) => f1.map(g).ap(f2));
const liftA3 = curry((g, f1, f2, f3) => f1.map(g).ap(f2).ap(f3));
// liftA4, etcliftA2 évoque un ascenseur capricieux ou une enseigne de limousine low-cost. Mais en réalité, ce nom décrit parfaitement son rôle : élever des valeurs dans le monde applicatif.
Bien que l'approche 2-3-4 semble intrusive, elle permet l'application partielle de liftA(N) elle-même.
Exemple pratique :
// checkEmail :: User -> Either String Email
// checkName :: User -> Either String String
const user = {
name: 'John Doe',
email: 'blurp_blurp',
};
// createUser :: Email -> String -> IO User
const createUser = curry((email, name) => { /* creating... */ });
Either.of(createUser).ap(checkEmail(user)).ap(checkName(user));
// Left('invalid email')
liftA2(createUser, checkEmail(user), checkName(user));
// Left('invalid email')Avec liftA2, le code devient générique et indépendant du type Either. La flexibilité s'en trouve accrue.
Réécriture des exemples précédents :
liftA2(add, Maybe.of(2), Maybe.of(3));
// Maybe(5)
liftA2(renderPage, Http.get('/destinations'), Http.get('/events'));
// Task('<div>some page with dest and events</div>')
liftA3(signIn, getVal('#email'), getVal('#password'), IO.of(false));
// IO({ id: 3, email: 'gg@allin.com' })Opérateurs
En Haskell, Scala ou Swift, on peut voir des opérateurs infixes :
-- Haskell / PureScript
add <$> Right 2 <*> Right 3// JavaScript
map(add, Right(2)).ap(Right(3));<$> équivaut à map, <*> à ap. Ce style permet une application fonctionnelle plus naturelle et réduit les parenthèses.
Outils prêts à l'emploi

Les interfaces dérivées héritent des lois de leurs parents. Une applicative étant d'abord un foncteur, on peut définir ce dernier à partir de la première.
Cette harmonie computationnelle découle du cadre mathématique sous-jacent. Mozart lui-même n'aurait pas fait mieux.
La relation of/ap == map permet de définir map gratuitement :
Les monades étant au sommet de la hiérarchie, chain nous donne foncteurs et applicatives gratuitement :
// map derived from of/ap
X.prototype.map = function map(f) {
return this.constructor.of(f).ap(this);
};Cette automatisation remarquable fournit tous ces outils de base. Attention cependant : utiliser chain pour implémenter ap sacrifie l'évaluation concurrente.
// map derived from chain
X.prototype.map = function map(f) {
return this.chain(a => this.constructor.of(f(a)));
};
// ap derived from chain/map
X.prototype.ap = function ap(other) {
return this.chain(f => other.map(f));
};Privilégiez les applicatives quand une monade n'est pas nécessaire : elles minimisent la charge cognitive en limitant les fonctionnalités superflues.
Les monades permettent le séquencement et le contrôle de flux via leur structure imbriquée. Les applicatives nous libèrent de ces contraintes.
Passons maintenant aux aspects légaux...
Lois
Les foncteurs applicatifs possèdent des propriétés fondamentales. Ils sont « fermés par composition » : ap ne modifie pas le type de conteneur. Cette stabilité typologique (contrairement aux monades) permet l'empilement d'effets multiples.
Démonstration :
Aucun risque de mélange typologique ici.
Examinons la loi d'identité :
const tOfM = compose(Task.of, Maybe.of);
liftA2(liftA2(concat), tOfM('Rainy Days and Mondays'), tOfM(' always get me down'));
// Task(Maybe(Rainy Days and Mondays always get me down))Identifier
Appliquer id depuis un foncteur doit laisser la valeur inchangée. Exemple :
Identity.of(id) illustre cette tautologie. Notons que of/ap équivaut à map, donc cette loi découle directement de l'identité fonctorielle.
// identity
A.of(id).ap(v) === v;Ces lois agissent comme un coach sportif catégorique, garantissant l'interopérabilité des interfaces.
const v = Identity.of('Pillow Pets');
Identity.of(id).ap(v) === v;Homomorphisme
Un homomorphisme préserve la structure. Toute application dans un conteneur doit équivaloir à son application externe suivie d'une mise en conteneur.
Exemple :
// homomorphism
A.of(f).ap(A.of(x)) === A.of(f(x));Interchange
La loi d'interchange stipule que l'ordre de « levage » dans ap est sans importance :
Composition
Either.of(toUpperCase).ap(Either.of('oreos')) === Either.of(toUpperCase('oreos'));Cette loi vérifie la préservation de la composition fonctionnelle standard dans les conteneurs.
En résumé
// interchange
v.ap(A.of(x)) === A.of(f => f(x)).ap(v);Les foncteurs applicatifs excellent quand on manipule multiples arguments dans des foncteurs. Bien que les monades puissent réaliser cela, privilégiez les applicatives en l'absence de besoin spécifique monadique.
const v = Task.of(reverse);
const x = 'Sparklehorse';
v.ap(Task.of(x)) === Task.of(f => f(x)).ap(v);Nous maîtrisons désormais map, chain et ap. Le prochain chapitre couvrira les transformations avancées entre foncteurs.
Chapitre 11 : Transformation à nouveau, naturellement
// composition
A.of(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w));const u = IO.of(toUpperCase);
const v = IO.of(concat('& beyond'));
const w = IO.of('blood bath ');
IO.of(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w));Exercices
Pratiquons !
Écrivez une fonction additionnant deux nombres potentiellement null avec Maybe et ap.
Pratiquons !
Réécrivez safeAdd (exercice_b) avec liftA2 au lieu de ap.
// safeAdd :: Maybe Number -> Maybe Number -> Maybe Number
const safeAdd = undefined;Pour l'exercice suivant, considérez ces helpers :
Pratiquons !
Créez une IO récupérant player1 et player2 du cache pour démarrer le jeu. // safeAdd :: Maybe Number -> Maybe Number -> Maybe Number const safeAdd = undefined;
Pour le prochain exercice, considérez les helpers suivants :
const localStorage = {
player1: { id:1, name: 'Albert' },
player2: { id:2, name: 'Theresa' },
};
// getFromCache :: String -> IO User
const getFromCache = x => new IO(() => localStorage[x]);
// game :: User -> User -> String
const game = curry((p1, p2) => `${p1.name} vs ${p2.name}`);Pratiquons !
Créez un IO qui récupère player1 et player2 du cache et lance le jeu. // startGame :: IO String const startGame = undefined;