Une nouvelle librairie monte petit à petit dans le vaste écosystème JavaScript. Elle est basée sur la programmation fonctionnelle, un pattern de développement qui fait un peu peur aux non-initiés.
Pour placer le lecteur dans le contexte, voici un exemple de programmation fonctionnelle en JavaScript : (en utilisant fp-ts, la librairie la plus connue qui le propose)
import { pipe } from "fp-ts/function";
const len = (s: string): number => s.length
const double = (n: number): number => n * 2
// avec pipe
assert.strictEqual(pipe('aaa', len, double), 6)
Une nouvelle librairie émerge, sa promesse ? Permettre d'appréhender la programmation fonctionnelle simplement, via des effets.
C'est le nom de cette librairie : Effect La doc de Effect
Qu'est-ce qu'un effet ?
Un effet est un programme encapsulé. Il gère son contexte, ses dépendances et les entrées comme les retours sont fortement typés.
Le tout utilisant le pattern fonctionnel.
Certains articles de blog parlent de la gestion d'erreur. Sachant qu'il y a en TypeScript beaucoup de manières d'éviter les try ... catch, Effect va beaucoup plus loin que cela.
Utiliser Effect, un game changer
Tout le monde peut écrire du JavaScript, quelques fonctions, un peu de document.getElementById et voilà, on devient développeur JavaScript.
Par contre, demandez à ce "développeur JavaScript" ce qu'il pense du code pur et vous le perdez en une minute.
C'est ici qu'Effect vous donne un véritable coup de fouet sur votre façon de générer du code propre.
Concrètement, un développeur capable d'implémenter Effect et d'en comprendre les enjeux se démarque des apprentis sorciers qui se prétendent développeurs JavaScript.
Un prérequis : la programmation fonctionnelle
Je suis familier de la programmation fonctionnelle depuis trois ans. Celle-ci m'a apporté certaines bonnes pratiques que j'utilise dans le code, même non fonctionnel :
Les fonctions pures
Une fonction pure est une fonction qui retourne toujours la même valeur pour les mêmes paramètres. Elle ne provoque pas d'effets de bord.
C'est clairement une bonne pratique à assimiler, votre code est plus robuste, plus lisible, plus testable.
Et c'est la base de la programmation fonctionnelle.
Voir l'article suivant sur le code robuste : Qu'est-ce qu'un code robuste en JavaScript ?
L'encapsulation des types (les monades)
Les éléments sont englobés dans un type qui peut être chaîné par la suite. Concrètement, cela donne :
import { Effect } from "effect"
const divide = (a: number, b: number): number => {
if (b === 0) {
throw new Error("Cannot divide by zero")
}
return a / b
}
// devient :
const divide = (a: number, b: number)
: Effect.Effect<number, Error, never> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
J'utilise volontairement l'exemple de la documentation de Effect, néanmoins, il est possible de remplacer cette encapsulation par la vôtre :
import { customEither as E } from "./myCustomEither"
const divide = (a: number, b: number)
: E<Error, number> =>
b === 0
? E.left(new Error("Cannot divide by zero"))
: E.right(a / b)
L'avantage ? À la place de ne pas maîtriser le typage de la gestion d'erreur, ici, le type de l'erreur est inclus dans l'encapsulation. C'est une valeur comme une autre.
Effect : mon test
Comme on l'a vu, la programmation fonctionnelle apporte de très bonnes pratiques de développement.
Plus qu'une librairie, un écosystème
Effect veut toucher le plus de monde possible, il y a donc beaucoup de fonctionnalités
- Le management d'erreurs
- Des wrappers pour beaucoup de méthodes très utiles (timeout, retry, fallback, ...).
- Un gestionnaire de schéma de données fonctionnant comme Zod.
- D'autres fonctionnalités expérimentales (FileSystem, gestion de cache, commande, ...)
Il va devenir théoriquement possible de pouvoir construire des applications complètes sans installer de dépendance autre que Effect, ou du moins, les limiter fortement.
Un point fort non négligeable est la présence d'une communauté active via un Discord jamais en panne de nouvelles interactions.
La prise en main
La documentation est très bien faite et la prise en main est relativement simple.
Du moment que l'on respecte les bonnes pratiques de programmation, cela va comme sur des roulettes. De toute façon, si ce n'est pas le cas, la compilation ne s'effectue pas.
C'est un avantage fondamental de l'encapsulation, l'ensemble de la chaîne d'exécution doit respecter le typage de la monade.
const divide = (a: number, b: number)
: T.Effect<number, Error, never> =>
b === 0 ? T.fail(new Error("Cannot divide by zero")) : T.succeed(a / b);
const add =
(entry: number) =>
(numberToAdd: number): T.Effect<number, never, never> =>
T.succeed(entry + numberToAdd);
const myAwesomeNumberEffect: T.Effect<number, Error, never> =
pipe(
divide(10, 2),
T.flatMap(add(1))
);
const myAwesomeNumber = T.runSync(myAwesomeNumberEffect);
assert.strictEqual(myAwesomeNumber, 6);
Il est impossible de pouvoir écrire quelque chose comme :
const divide = (a: number, b: number): T.Effect<number, Error, never> =>
b === 0 ? T.fail(new Error("Cannot divide by zero")) : T.succeed(a / b);
const add =
(entry: number) =>
(numberToAdd: number): T.Effect<number, never, never> =>
T.succeed(entry + numberToAdd);
const dummyThink = (entry: number) =>
T.succeed({
foo: "oops",
entry
});
const myAwesomeNumberEffect = pipe(
divide(10, 2),
T.flatMap(dummyThink),
// Argument of type '<E, R>(self: Effect<number, E, R>) => Effect<number, E, R>' is not assignable to parameter of type '(b: Effect<{ foo: string; entry: number; }, Error, never>) => Effect<number, Error, never>'.
T.flatMap(add(1)),
);
Cela peut prêter à sourire, sachez que l'on voit des dingueries dans certaines bases de code.
Les inconvénients
Maintenant, parlons du sujet qui fâche.
Voici un exemple de code récupéré sur le Discord de la communauté, posté par un utilisateur :
const myApiBasePath = '/custom-base'
const serve = HttpApiBuilder.httpApp.pipe(
Effect.map(app =>
HttpRouter.empty.pipe(
HttpRouter.mountApp(myApiBasePath, app),
)
),
Effect.map(HttpServer.serve(HttpMiddleware.logger)),
Layer.unwrapEffect,
Layer.provide(HttpApiBuilder.Router.Live),
)
Je ne vais pas entrer dans les détails de l'implémentation, ni dire si elle est bonne ou pas.
Ce que je peux dire ici, c'est que la lecture de ce code nécessite d'être familiarisé avec la programmation fonctionnelle et Effect.
C'est quelque chose qui peut rebuter des développeurs expérimentés, voire parfois provoquer des décisions de réécriture (Un cas concret a été détaillé sur le Discord : une réécriture sans Effect pour la "lisibilité")
J'ai donc l'impression que la communauté Effect est telle Don Quichotte se battant contre les moulins à vent.
La paresse qui gangrène l'écosystème JavaScript crée un nivellement par le bas de la qualité du code. Parfois voulu par des ESN peu scrupuleuses.
L'orientation de JavaScript n'est pas fonctionnelle
Même s'il y a un peu de fonctionnel dans les fonctionnalités standards de JavaScript, lorsque l'on consulte les évolutions sur https://tc39.es/, on constate que la programmation fonctionnelle n'en fait pas partie.
Il y a des propositions d'opérateurs visant à améliorer la gestion d'erreur ( ?= ) et TypeScript a encore à faire pour le typage des try ... catch.
Le piège de la dette technique
Effect force les bonnes pratiques. Néanmoins, comme tout framework, il est possible de mal l'utiliser.
Il y a trois ans, mon équipe et moi avons récupéré une application écrite dans l'ancêtre d'Effect : Matechs, une librairie expérimentale qui est le prototype du prototype de Effect.
Le code était entièrement écrit avec cette librairie, sans réel design pattern, sans centralisation de style de codage. Bref un énorme plat de spaghetti.
Il était devenu impossible d'ajouter la moindre fonctionnalité.
Il faut donc rester très vigilant sur le choix d'un écosystème de codage avant de l'implémenter. Si celui-ci est réalisé par des personnes ne respectant pas les bases de la programmation, vous serez endetté durant de très longues années.