Skip to content

Chapitre 06 : Exemple d'application

Programmation déclarative

Nous allons opérer un changement de paradigme. À partir de maintenant, nous cesserons de dicter à l'ordinateur comment procéder, pour plutôt décrire spécifiquement le résultat souhaité. Vous verrez que cette approche est bien moins stressante que de tout microgérer constamment.

Une approche déclarative, opposée à l'impérative, consiste à écrire des expressions plutôt que des instructions séquentielles.

Prenons l'exemple du SQL. Il n'y a pas de « premier ceci, puis cela ». Une seule expression spécifie le résultat souhaité de la base de données. Nous ne spécifions pas la méthode d'exécution - c'est le moteur SQL qui s'en charge. Lors des mises à jour du moteur, nos requêtes restent inchangées car la spécification permet d'atteindre le résultat par différents moyens.

Pour beaucoup, notamment moi-même au début, le concept de programmation déclarative peut être déroutant. Analysons donc quelques exemples concrets pour en saisir l'essence.

js
// imperative
const makes = [];
for (let i = 0; i < cars.length; i += 1) {
  makes.push(cars[i].make);
}

// declarative
const makes = cars.map(car => car.make);

La boucle impérative nécessite d'instancier manuellement un tableau avant itération. L'interpréteur exécute ces instructions séquentiellement, avec compteur explicite et manipulation directe des éléments - une démonstration explicite d'itération procédurale.

La version avec map n'est qu'une expression unique. Son ordre d'exécution est non contraint. Cette abstraction permet au moteur d'optimiser librement l'itération et la construction du tableau résultat, spécifiant le quoi plutôt que le comment. Elle arbore ainsi fièrement l'étendard déclaratif.

Outre sa clarté et concision, la fonction map permet des optimisations transparentes sans impact sur le code applicatif.

Pour les sceptiques évoquant les performances des boucles impératives, je recommande cette excellente vidéo sur les optimisations JIT qui apporte un éclairage pertinent.

Voici un autre exemple.

js
// imperative
const authenticate = (form) => {
  const user = toUser(form);
  return logIn(user);
};

// declarative
const authenticate = compose(logIn, toUser);

Bien que fonctionnellement correcte, la version impérative impose une évaluation séquentielle figée. L'expression compose exprime quant à elle une relation logique : l'authentification est la composition de toUser et logIn. Cette abstraction permet des ajustements d'implémentation tout en maintenant une spécification applicative de haut niveau.

Dans l'exemple ci-dessus, l'ordre d'exécution est spécifié (toUser doit être appelé avant logIn), mais dans de nombreux cas cet ordre est non pertinent. Le codage déclaratif permet alors d'exprimer naturellement cette indépendance (nous y reviendrons).

L'absence de séquence imposée rend le code déclaratif intrinsèquement compatible avec le calcul parallèle. Combiné aux fonctions pures, ceci explique pourquoi la programmation fonctionnelle est idéale pour l'ère du parallélisme - aucun effort particulier n'est requis pour concevoir des systèmes concurrents.

Une Étincelle de Programmation Fonctionnelle avec Flickr

Nous allons développer une application démonstrative selon les principes déclaratifs et compositionnels. Nous utiliserons temporairement des effets de bord, mais les isolerons de la logique pure. Notre objectif est un widget navigateur agrégant des images Flickr. Commençons par le squelette HTML :

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Flickr App</title>
  </head>
  <body>
    <main id="js-main" class="main"></main>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.2.0/require.min.js"></script>
    <script src="main.js"></script>
  </body>
</html>

Voici la structure principale en main.js :

js
const CDN = s => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
const ramda = CDN('ramda/0.21.0/ramda.min');
const jquery = CDN('jquery/3.0.0-rc1/jquery.min');

requirejs.config({ paths: { ramda, jquery } });
requirejs(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => {
  // app goes here
});

Nous utilisons ramda plutôt que lodash pour ses fonctions compose, curry, etc. Bien que Requirejs puisse sembler superflu ici, sa cohérence d'utilisation dans l'ouvrage justifie son emploi.

Passons aux spécifications fonctionnelles. Notre application réalisera quatre étapes clés :

  1. Construire une URL pour le terme de recherche
  2. Interroger l'API Flickr
  3. Transformer le JSON résultat en images HTML
  4. Afficher ces images à l'écran

Deux opérations impures sont identifiées : l'appel API et la manipulation DOM. Définissons-les explicitement pour les isoler. Ajoutons également une fonction trace de débogage :

js
const Impure = {
  getJSON: curry((callback, url) => $.getJSON(url, callback)),
  setHtml: curry((sel, html) => $(sel).html(html)),
  trace: curry((tag, x) => { console.log(tag, x); return x; }),
};

Nous avons curryfié les méthodes jQuery et réorganisé leurs arguments pour meilleure ergonomie. Le namespace Impure signale clairement leur nature à risque. Une future itération purifiera ces fonctions.

Élaborons maintenant l'URL pour Impure.getJSON :

js
const host = 'api.flickr.com';
const path = '/services/feeds/photos_public.gne';
const query = t => `?tags=${t}&format=json&jsoncallback=?`;
const url = t => `https://${host}${path}${query(t)}`;

Bien qu'une écriture en notation pointfree (sans variable explicite) sophistiquée soit possible via des monoïdes ou combinateurs, nous privilégions ici une version lisible avec construction classique de chaîne.

Définissons une fonction applicative effectuant l'appel et l'affichage :

js
const app = compose(Impure.getJSON(Impure.trace('response')), url);
app('cats');

Cette fonction génère l'URL, l'envoie à getJSON (pré-curryfié avec trace). Le chargement affichera la réponse API dans la console.

console response

Pour extraire les URLs d'images du JSON, notons qu'elles sont imbriquées dans items puis media.m. Utilisons prop de Ramda comme accesseur universel :

Version customisée pour clarification :

js
const prop = curry((property, object) => object[property]);

Simple accès propriétaire via []. Appliquons ceci à mediaUrls :

js
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));

Après extraction des items, un map permet d'extraire chaque URL. Intégrons ce flux à notre application :

js
const render = compose(Impure.setHtml('#js-main'), mediaUrls);
const app = compose(Impure.getJSON(render), url);

Nous combinons mediaUrls avec render pour remplacer trace. Les URLs s'affichent maintenant brutalement dans le body.

Pour transformer ces URLs en véritables images certifiées, utiliserions Handlebars/React dans une grosse app. Ici, un simple tag img avec jQuery suffit :

js
const img = src => $('<img />', { src });

La méthode html() de jQuery acceptant des tableaux de tags, transformons nos URLs en images avant envoi :

js
const images = compose(map(img), mediaUrls);
const render = compose(Impure.setHtml('#js-main'), images);
const app = compose(Impure.getJSON(render), url);

Terminé !

cats grid

Script final : include

Remarquons cette spécification déclarative élégante décrivant l'essence plutôt que le process. Chaque ligne devient une équation aux propriétés vérifiables, base solide pour le raisonnement et le refactoring.

Refactoring Méthodique

Optimisation possible via une loi de composition des maps : deux itérations successives (URLs puis images) peuvent fusionner en une :

js
// map's composition law
compose(map(f), map(g)) === map(compose(f, g));

Appliquons cette propriété pour un refactoring rigoureux.

js
// original code
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
const images = compose(map(img), mediaUrls);

Alignons nos maps. L'équivalence fonctionnelle permet d'inliner mediaUrls dans images :

js
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(img), map(mediaUrl), prop('items'));

L'alignement permet d'appliquer la loi de composition :

js
/*
compose(map(f), map(g)) === map(compose(f, g));
compose(map(img), map(mediaUrl)) === map(compose(img, mediaUrl));
*/

const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(compose(img, mediaUrl)), prop('items'));

Désormais, une seule itération transforme les items en images. Améliorons la lisibilité par extraction fonctionnelle :

js
const mediaUrl = compose(prop('m'), prop('media'));
const mediaToImg = compose(img, mediaUrl);
const images = compose(map(mediaToImg), prop('items'));

Synthèse

Nous avons appliqué nos concepts à une application réaliste. Le cadre mathématique facilite raisonnement et refactoring. Les prochains défis incluront la gestion d'erreurs, la pureté totale et l'expressivité accrue - sujets du prochain chapitre.

Chapitre 07 : Hindley-Milner et moi