Haskell et programmation fonctionnelle, ma conviction

J’ai promis ce blog post à quelques développeurs de mes amis, de trop longue date. À ceux-là je m’excuse du retard au décollage.

Pourquoi ?

J’ai fait le pari de Haskell pour les services métier de FretLink. Et ce choix est non-orthodoxe. Je n’ai pas de statistiques robustes, mais de tête je dirais qu’une startup créée entre 2015 et 2018 utilise nodejs dans 1 cas sur 3, php 1 cas sur 3, et le reste se partage entre Rails, Go et Python.

Alors je me sens obligé de présenter mes raisons. Je me contenterai d’expliquer pourquoi ma préférence va à Haskell, et non pas pourquoi je déconseille nodejs ou php. Je réserve cette prose aux buffets des meetups et conférences.

J’aurais tellement d’arguments à présenter qu’il faut trier. Et j’ai donc choisi de vous illustrer cela par le quotidien d’une startup.

Un peu de contexte

Quand je parle développement logiciel, c’est toujours avec passion, comme bon nombre d’entre nous dans ce métier. De l’extérieur ça peut des fois paraître excessif.

En ce qui me concerne cette passion s’est transformée avec les années, au fil des frustrations, des découvertes techniques, des rencontres avec des collègues de qualité (coucou les Capitaines). J’ai petit à petit dérivé de l’ingénierie vers les questions de qualité logicielle. Et la qualité passe par les outils et les méthodes.

Parlons d’agile

Les méthodes d’abord. Elles sont clefs et dépassent notre métier. Elles se mettent en oeuvre par les échanges avec les autres équipes, ces observateurs et témoins des besoins utilisateurs.

Je ne parlerai pas de post-its, de Scrum master, de Product Owner. Tout ceci est dogmatique et je vous laisse vous faire votre avis.

Mais je pourrais vous parler d’expression de besoin, de maquettes, de retours utilisateurs, d’A/B testing. Ce qui fait la pratique de ces méthodes.

Vous connaissez sans doute cela si vous avez un UX designer, un équipe support, une équipe marketing, des commerciaux. Leur pratique de l’agilité est plus ou moins consciente, et nous développeurs sommes dépendants d’eux. Ils sont nos détectives pour comprendre nos utilisateurs, élucider ce qu’eux-mêmes ne savent pas souvent formuler. Un travail d’enquête.

Et quand bien même ce travail d’enquête fut-il complet, il vous reste à mettre tout cela debout et produire un logiciel qui fonctionne.

L’exécution

Celle-ci commence par la modélisation du problème, son expression en concept qu’un ordinateur peut manipuler.

Une fois ce modèle défini il vous est possible d’écrire une solution au problème posé. On peut alors faire intervenir des algorithmes, objets de fantasmes depuis quelques années (lecteur du futur, j’écris ceci en janvier 2018).

A mes débuts, je me projetais sur l’algorithme avec la gourmandise d’un geek face à un casse-tête (ou un set Lego de 3000 pièces, choisissez votre analogie préférée). C’était ce qui m’attirait : résoudre un problème, obtenir la solution.

Puis j’ai dû mettre du code en production. Ouille.

Le code en production doit être maintenu, testé, il doit fonctionner. Et ma capacité à trouver des solutions habiles fut mise à l’épreuve des cas marginaux, des erreurs, des saisies invalides, des montées de version, de la collaboration avec d’autres développeurs.

La maintenance attira donc mon attention de plus en plus, les années passant.

J’ai rencontré l’agile, certes, mais j’ai aussi rencontré de nouveaux outils et concepts.

IDEs

Mes premières années professionnelles était l’âge du Java resplendissant, la grande époque de Eclipse vs IntelliJ. Une époque formidable, où l’on devait apprendre 2 douzaines de raccourcis pour profiter des fonctionnalités de refactoring de ces environnements.

Puis j’ai dû écrire du Ruby, et je suis retourné à Vim, pour faire revivre la grande époque du Vim vs Emacs. Une époque formidable, où l’on devait apprendre 4 douzaines de raccourcis pour profiter des fonctionnalités de traitement de texte. Oubliez le refactoring automatisé. Une sorte de retour à l’âge de pierre. J’ai dû compenser en écrivant des tests, en nommant mieux les choses, en découpant, en isolant, en évitant les effets de bord.

J’étais frustré. L’absence de compilateur est probablement ce qui me rendait le plus triste. On écrivait des bugs en étant agile. Restait donc à régler cette histoire de bugs.

Pour vous donner envie

OK cette transition est cavalière, et pourrait faire croire que je vais vous présenter Haskell comme sauveur.

Rassurez-vous, on écrit de très jolis bugs en Haskell, la différence est qu’ils sont compilés, sans effet de bord, et lazy par défaut. Des bugs modernes.

Et pourtant écrire du Haskell fut pour moi le plus grand pas en avant que j’ai pu faire en changeant de langage de programmation.

Laissez-moi vous donner quelques exemples concrets et autant que possible accessibles.

Types algébriques

Écrire un algorithme qui fonctionne, ça se fait bien, quelques tests unitaires et vous êtes paré. Décrire votre modèle métier, votre couche de persistance, les clients d’API tierces c’est autrement plus pénible à maintenir et tester.

Et à dire vrai, ça ne se teste pas vraiment, ça se compile. Quand vous avez un compilateur, et que vous pouvez modéliser correctement votre domaine métier.

L’exercice de modélisation fut longtemps synonyme de classes et d’objets en POO. Ajoutez un peu d’UML et vous avez la boite à outils idéale.

Enfin, c’est ce que j’avais appris à l’école. On ne m’avait pas appris à écrire des tests ou ce que pouvait être la programmation fonctionnelle. J’ai “échappé” aux cours de OCaml, et aujourd’hui je le regrette.

J’aurais dû apprendre les types algébriques. Haskell n’en est pas le seul héraut bien sûr. Je me contente d’en profiter sans vanité et je souhaite à tous les langages de programmation modernes de les supporter.

En 2 mots de quoi s’agit-il ? Les types algébriques permettent de représenter une information en combinant les types produits et sommes. Vous connaissez déjà ces concepts. Les sommes sont les types énumérés. Les produits sont les structures de données telles que les struct en C ou les classes en Java.

-- Type énuméré, aka sum type 
data
Color = Red | Blue | Green
-- Structure de données, aka product type
data PixelRGB = PixelRGB Color Color Color

Il est possible de combiner les 2 aspects dans un type plus complexe :

data PixelRGB = PixelRGB Color Color Color
| DeadPixel

Je vous laisse imaginer ce que cela représente. Si vous êtes développeur expérimenté, je peux comprendre que vous tiquiez sur la syntaxe. Mais si vous êtes débutant, et surtout si vous n’est pas développeur, vous devriez comprendre ce que cela signifie.

Du code accessible

Ce fut pour moi une révélation puis un argument fort pour ce langage : rendre accessible la partie du code qui décrit le mode et que des non développeurs devraient pouvoir comprendre.

Avec quelques explications de syntaxe, vous pouvez utiliser une série de datatypes Haskell comme support de discussion dans le cadre d’une expression de besoin avec un interlocuteur non technique.

data User =
User { firstName :: Text
, lastName :: Text
, profilePicture :: Maybe Picture
}
-- Cachez les types concrets, nommez les concepts
-- c'est de toute façon une bonne pratique
type Picture = URL

Les types algébriques sont aussi un bon outil pour décrire un domaine quel qu’il soit, y compris données dans une base SQL. Vous serez probablement plus précis dans un langage qui compile et dont la syntaxe est supportée par beaucoup d’environnements de développement.

Le code “métier” lui-même peut aussi être accessible. Nul besoin de eDSL pour permettre à votre expert de valider ce que vous avez écrit.

Chez FretLink, nous travaillons à rendre les règles métier de notre pricer lisibles par notre Product Manager qui n’est pas développeur. Mais il sait lire des papiers de recherche, alors quelques fonctions écrites correctement devraient lui être accessibles.

Si vous faites l’effort de séparer la tuyauterie (SQL, HTTP, sérialisation, validation, caches et autres) du code et des concepts, ça devrait bien se passer.

null || undefined

OK soyons un poil provocateur : pourquoi a-t-on encore des bugs sur des valeurs non définies, the dreadful NullPointerException ?

J’ai passé l’âge de ces bêtises, et j’attends d’un ordinateur qui a la patience infinie et la force de fouiller tous les détails de mon code pour vérifier pour moi qu’une expression soit totalement définie.

Ceci est rendu possible ainsi :

  • toute expression a une valeur dans son type. Il n’existe pas de null universel. Si vous avez besoin de représenter l’absence d’une valeur utilisez Maybe (l’option en Haskell).
  • le compilateur peut vérifier pour vous qu’une fonction est totalement définie : il doit exister du code pour toutes les valeurs possibles des paramètres d’une fonction.

Avec ceci j’ai la garantie que mon code gérera tous les cas, y compris les cas “pénibles” que l’on a tendance à négliger.

Evidemment tout le code Haskell que vous pourriez utiliser n’a pas forcément rempli cette condition, il faut rester vigilant donc. Mais vous êtes maître de ce que vous écrivez, et la discipline de groupe de l’équipe fera le reste.

Tests de propriétés

Vous aimez écrire des tests ? J’entends par là des tests unitaires. On écrit rarement autre chose (les RSpec et autres ne sont que des tests unitaires exprimés dans un DSL). Ces tests sont pénibles à écrire car vous humains devez vous poser des questions sur les jeux de test et les assertions correspondantes.

Cet ensemble de jeux de tests et assertions a pour rôle de valider les règles que le code testé doit implémenter. Il existe très souvent une alternative bien plus efficace, confortable et correcte pour tester son code : énoncer des propriétés et laisser Haskell générer des cas de tests pour vous.

Je parle là de QuickCheck.

Si vous deviez tester une fonction qui, pour une liste d’éléments quelconque, renvoie une liste dont les éléments sont en position contraire, vous pourriez écrire le test unitaire suivant :

test_reverse = reverse [1, 2, 3] @?= [3, 2, 1]

Vous auriez alors aussi besoin d’écrire un test pour la liste vide, puis pour une liste avec beaucoup de valeurs, 1 seule valeur, que sais-je. Ceci fait intervenir votre assiduité et votre expérience.

Mais quelles sont les propriétés de reverse qui nous intéressent ?

prop_reverse_identity :: [Int] -> Bool
prop_reverse_identity items = reverse (reverse items) == items
prop_reverse_length :: [Int] -> Bool
prop_reverse_length items = length (reverse items) == length items
prop_reverse_sum :: [Int] -> [Int] -> Bool
prop_reverse_sum xs ys = reverse (xs ++ ys) == reverse ys ++ reverse xs

On exprime ainsi que reverse reverse est la fonction identité, que reverse ne change pas la taille de la liste (cette propriété peut être déduite de la première, mais ceci peut aussi être intéressant à des fins de documentation), et que reverse de la concaténation de 2 listes et la concaténation des reverse des 2 listes échangées.

Notez également qu’on peut réécrire ces 2 propriétés d’une façon encore plus expressive si on s’équipe d’une fonction utilitaire :

behavesLike :: Eq a => (a -> b) -> (a -> b) -> a -> Bool
behavesLike f1 f2 a = f1 a == f2 a
prop_reverse_identity :: [Int] -> Bool
prop_reverse_identity = reverse . reverse `behavesLike` id
prop_reverse_length :: [Int] -> Bool
prop_reverse_length = length . reverse `behavesLike` length

Si vous mettez de côté la signature de la fonction behavesLike, vous pourrez revenir sur ces propriétés dans 2 ou 6 mois et comprendre au premier regard ce qu’elles expriment. A titre personnel, c’est un des bénéfices les plus marquants d’un langage comme Haskell.

Polymorphisme et programmation fonctionnelle

La fonction behavesLike est facile à écrire grâce au support très poussé du polymorphisme et des concepts de programmation fonctionnelle.

Pouvoir passer des fonctions en paramètres d’une autre fonction est ce qui rend un algorithme plus général. Vous avez l’habitude de trier une liste en passant en paramètre un critère de tri. Ce critère n’est qu’une fonction de comparaison.

Écrire du code générique c’est le rendre indépendant des cas particuliers dont il émerge, et le rendre ainsi plus robuste. Quand vous manipulez une liste d’éléments et que vous ignorez tout de ces éléments, vous ne pouvez pas les altérer par erreurs. Le système de types vous évite des erreurs.

L’exemple de propriétés QuickCheck sur la fonction reverse manipule des listes d’entiers, un cas particulier donc. C’est acceptable car on sait par polymorphisme que reverse est indifférent aux éléments contenus :

reverse :: [a] -> [a]

reverse ne sait rien des éléments a, il ne peut pas les modifier. Il ne peut pas non plus remplacer les valeurs par null, car null n’existe pas ! Il peut seulement agir sur la liste, en changeant sa taille par exemple, ou l’ordre des éléments.

More Haskell please

Il y aurait beaucoup plus à dire bien sûr, mais je ne prendrai pas le risque d’écrire un livre sur le sujet quand tant de bonnes ressources existent ici ou ou encore là.

Je recrute (surprise !)

https://fretlink.com/jobs si vous êtes plutôt email et CV, https://twitter.com/ptit_fred si vous êtes plutôt DM et memes

Je recrute en backend Haskell + node (faut bien maintenir l’existant), et en frontend (React, Purescript, ça dépend).

Faire connaître et aimer Haskell

Je prêche pour ma paroisse bien sûr. Sinon je ne serais pas sur medium à 3h du mat’ alors que je dois me lever tôt demain…

J’aimerais que plus de développeurs s’intéressent à la programmation fonctionnelle, et à Haskell, Purescript, Idris, Elm, et d’autres. Par conviction, par amour de mon métier, pour débarrasser ce métier de frustrations absurdes qui font que certains finissent par s’en échapper, pour redorer le blason de cette profession qui se traîne une réputation de producteur de bugs, et par passion !

J’aime ce langage, et j’en ferai la publicité avec mes modestes moyens. C’est pourquoi je me suis lancé sur une série de livecodings pour débutants sur Twitch (rediffusions sur Youtube).

J’en parle aussi en conférences comme en novembre 2017 à ScalaIO à Lyon en compagnie de Clément Delafargue :

Pour les parisiens je passe également régulièrement au meetup Haskell Paris, et je reste généralement pour les bières et discuter. N’hésitez pas à venir discuter à l’occasion !