Skip to content

Chapitre 05 : Programmation par Composition

Sélection Fonctionnelle

Voici compose :

js
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];

... Ne vous inquiétez pas ! Ceci est la forme niveau-9000-super-saiyan* de compose. [*NdT : référence à la transformation mythique dans Dragon Ball Z] Pour faciliter la compréhension, abandonnons l'implémentation variadique et considérons une version combinant deux fonctions. Une fois ce principe maîtrisé, l'abstraction pourra être étendue (preuve à l'appui !) Voici une version plus abordable à l'intention de nos lecteurs :

js
const compose2 = (f, g) => x => f(g(x));

f et g sont des fonctions, et x est la valeur « canalisée » à travers elles.

La composition s'apparente à un croisement fonctionnel. Vous sélectionnez deux spécimens dotés de caractéristiques complémentaires que vous fusionnez pour créer une nouvelle entité. Utilisation :

js
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const shout = compose(exclaim, toUpperCase);

shout('send in the clowns'); // "SEND IN THE CLOWNS!"

La composition génère une nouvelle fonction. C'est cohérent : combiner deux unités d'un type (ici des fonctions) produit une unité du même type. On ne connecte pas deux Lego pour obtenir une poutre d'assemblage. Une théorie sous-jacente se révèlera plus tard.

Dans notre définition de compose, g s'exécute avant f, établissant un flux droite-à-gauche. Plus lisible qu'un emboîtement d'appels. Sans compose :

js
const shout = x => exclaim(toUpperCase(x));

Au lieu d'intérieur-extérieur, nous procédons droite-à-gauche. Exemple de séquence cruciale :

js
const head = x => x[0];
const reverse = reduce((acc, x) => [x, ...acc], []);
const last = compose(head, reverse);

last(['jumpkick', 'roundhouse', 'uppercut']); // 'uppercut'

reverse inverse la liste tandis que head capture le premier élément. Ce qui implémente une fonction last efficace bien qu'inefficiente. L'ordre de composition est explicite. Notre version respecte la définition mathématique originelle.

js
// associativity
compose(f, compose(g, h)) === compose(compose(f, g), h);

La composition est associative. Pour convertir une chaîne en majuscules :

js
compose(toUpperCase, compose(head, reverse));
// or
compose(compose(toUpperCase, head), reverse);

Le regroupement étant neutre, une version variadique devient possible :

js
// previously we'd have to write two composes, but since it's associative, 
// we can give compose as many fn's as we like and let it decide how to group them.
const arg = ['jumpkick', 'roundhouse', 'uppercut'];
const lastUpper = compose(toUpperCase, head, reverse);
const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);

lastUpper(arg); // 'UPPERCUT'
loudLastUpper(arg); // 'UPPERCUT!'

Les bibliothèques lodash, underscore et ramda utilisent cette implémentation variadique.

L'associativité permet l'extraction et le regroupement libre de fonctions. Exemple de refactoring :

js
const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);

// -- or ---------------------------------------------------------------

const last = compose(head, reverse);
const loudLastUpper = compose(exclaim, toUpperCase, last);

// -- or ---------------------------------------------------------------

const last = compose(head, reverse);
const angry = compose(exclaim, toUpperCase);
const loudLastUpper = compose(angry, last);

// more variations...

Aucune méthodologie rigide - nous combinons librement des briques. Recommandation : regrouper les éléments réutilisables. L'«extraction de fonction» de Fowler s'applique ici, sans gestion d'état complexe.

Style Pointfree

Le style pointfree s'affranchit de toute mention explicite des données. Fonctions premières, currying et composition permettent cette abstraction pure.

Astuce : Les versions pointfree de replace/toLowerCase sont dans l'Annexe C - Utilitaires Pointfree.

js
// not pointfree because we mention the data: word
const snakeCase = word => word.toLowerCase().replace(/\s+/ig, '_');

// pointfree
const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

Notez l'application partielle de replace. Nous canalisons les données à travers des fonctions unaires. Le currying permet ce flux transformationnel. La version pointfree abstrait les données au moment de la construction.

Autre exemple :

js
// not pointfree because we mention the data: name
const initials = name => name.split(' ').map(compose(toUpperCase, head)).join('. ');

// pointfree
// NOTE: we use 'intercalate' from the appendix instead of 'join' introduced in Chapter 09!
const initials = compose(intercalate('. '), map(compose(toUpperCase, head)), split(' '));

initials('hunter stockton thompson'); // 'H. S. T'

Le pointfree élimine les identifiants superflus et garantit des transformations pures. Attention : Peut nuire à la lisibilité. Nous l'utiliserons avec discernement.

Débogage

Erreur courante : composer map (fonction binaire) sans application partielle.

js
// wrong - we end up giving angry an array and we partially applied map with who knows what.
const latin = compose(map, angry, reverse);

latin(['frog', 'eyes']); // error

// right - each function expects 1 argument.
const latin = compose(map(angry), reverse);

latin(['frog', 'eyes']); // ['EYES!', 'FROG!'])

En cas de difficulté, utilisez la fonction utilitaire trace (impure) :

js
const trace = curry((tag, x) => {
  console.log(tag, x);
  return x;
});

const dasherize = compose(
  intercalate('-'),
  toLower,
  split(' '),
  replace(/\s{2,}/ig, ' '),
);

dasherize('The world is a vampire');
// TypeError: Cannot read property 'apply' of undefined

Problème détecté ? Utilisons trace :

js
const dasherize = compose(
  intercalate('-'),
  toLower,
  trace('after split'),
  split(' '),
  replace(/\s{2,}/ig, ' '),
);

dasherize('The world is a vampire');
// after split [ 'The', 'world', 'is', 'a', 'vampire' ]

Ah ! Appliquer map à toLower pour les tableaux.

js
const dasherize = compose(
  intercalate('-'),
  map(toLower),
  split(' '),
  replace(/\s{2,}/ig, ' '),
);

dasherize('The world is a vampire'); // 'the-world-is-a-vampire'

trace permet l'inspection de données. Haskell/PureScript ont des fonctions similaires.

La composition, outil central de construction logicielle, bénéficie de garanties théoriques solides.

Théorie des Catégories

Branche mathématique unifiant ensembles, types, groupes et logique. Manipule objets/morphismes - parallèle frappant avec la programmation.

category theory

Aucune attente de maîtrise. Objectif : montrer la redondance interdisciplinaire expliquant l'ambition unificatrice.

Une catégorie se définit par :

  • Une collection d'objets
  • Une collection de morphismes
  • Une composition de morphismes
  • Un morphisme identité

Appliquons ce cadre aux types et fonctions.

Objets Types de données (String, Boolean...). Perçus comme ensembles de valeurs. Utilité de la théorie des ensembles.

Morphismes Fonctions pures standards.

Composition Notre fonction compose. Son associativité est une exigence théorique.

Schéma de composition :

category composition 1category composition 2

Exemple codé :

js
const g = x => x.length;
const f = x => x === 4;
const isFourLetterWord = compose(f, g);

Identité La fonction id retourne son argument :

js
const id = x => x;

Son utilité se révélera progressivement. Pour l'instant, valeur fonctionnelle caméléon.

Propriété clé pour toute fonction unaire f :

js
// identity
compose(id, f) === compose(f, id) === f;
// true

Analogique à l'identité multiplicative. id sera omniprésent. Substitut valoriel en pointfree.

Notre catégorie types-fonctions. Principes structurants : associativité et identité.

Autres catégories : graphes orientés (nœuds-objets, arêtes-morphismes) ou nombres avec >=. Focus sur notre domaine actuel.

En Résumé

La composition est un réseau de pipelines fonctionnels. Les données circulent inéluctablement à travers les transformations pures.

Principe architectural suprême. La théorie des catégories outillera l'architecture et la validation formelle.

Passons à un cas pratique concret.

Chapitre 06 : Exemple d'Application

Exercices

Objets Voiture utilisés dans les exercices :

js
{
  name: 'Aston Martin One-77',
  horsepower: 750,
  dollar_value: 1850000,
  in_stock: true,
}

Pratiquons !

Réécrivez avec compose(). const isLastInStock = (cars) => { const lastCar = last(cars); return prop('in_stock', lastCar); };


Fonction donnée :

js
const average = xs => reduce(add, 0, xs) / xs.length;

Pratiquons !

Réécrivez averageDollarValue via composition avec average. const averageDollarValue = (cars) => { const dollarValues = map(c => c.dollar_value, cars); return average(dollarValues); };


Pratiquons !

Refactorez fastestCar en pointfree avec compose(). Astuce : append. const fastestCar = (cars) => { const sorted = sortBy(car => car.horsepower, cars); const fastest = last(sorted); return concat(fastest.name, ' is the fastest'); };