Retour à l'accueil

JS/TS n'est pas un langage fonctionnel : analyse technique

L'article explique pourquoi JavaScript et TypeScript ne sont pas des langages de programmation fonctionnelle. Quatre limitations techniques clés sont considérées : mutabilité par défaut, absence d'optimisation de la récursion terminale, absence de collections paresseuses et de correspondance de motifs structurelle, ainsi que la gestion d'erreurs par exceptions. Des exemples et des conséquences pour le développement sont fournis.

JS/TS n'est pas un langage fonctionnel : comment les limitations techniques affectent votre code
Advertisement 728x90

JS/TS n’est pas un langage fonctionnel : Limitations techniques et leurs conséquences

De nombreux développeurs pensent à tort que JavaScript et TypeScript sont des langages fonctionnels en raison de méthodes comme .map() et .filter(). Cependant, ces langages ne prennent pas en charge les principes clés de la programmation fonctionnelle, ce qui entraîne des bugs cachés dans le code de production. Décomposons quatre raisons techniques expliquant pourquoi JS/TS n’est pas un langage fonctionnel et comment cela impacte le développement.

Mutabilité par défaut : L’illusion de l’immutabilité

Dans les langages fonctionnels comme Haskell ou Clojure, l’immutabilité est intégrée au langage. Par exemple, en Clojure, toutes les structures de données de base sont immuables par défaut. En JavaScript, en revanche, la mutabilité est le comportement par défaut. Même la déclaration avec const ne garantit pas l’immutabilité :

const user = { name: "Alice" };
user.name = "Bob"; // Fonctionne sans erreur

Ici, const n’empêche que la réassignation de la variable, pas la mutation du contenu de l’objet. Cela rompt la transparence référentielle — une propriété fondamentale de la programmation fonctionnelle qui permet de remplacer une expression par sa valeur sans modifier le comportement du programme.

Google AdInline article slot

Les tentatives d’imiter l’immutabilité avec Object.freeze() ou des bibliothèques comme Immutable.js ne sont que des palliatifs. Elles ne sont pas intégrées au langage et nécessitent des décisions explicites de l’équipe, limitant leur adoption. La raison de cette conception est historique : JavaScript a été créé en 10 jours en 1995 comme un langage de script simple pour les navigateurs, où la mutabilité simplifiait l’implémentation et correspondait aux habitudes des programmeurs C/Java. Redessiner le modèle de mémoire aujourd’hui sans casser le web est impossible.

Pas d’optimisation des appels de queue : Le coût du dynamisme

L’optimisation des appels de queue (TCO), proposée dans la norme ES2015, n’est toujours pas prise en charge dans les principaux moteurs sauf Safari. Dans Chrome et Firefox, une récursion profonde provoque un débordement de pile :

function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc);
}
factorial(100000); // RangeError: Maximum call stack size exceeded

En Scala, la même fonction avec l’annotation @tailrec est garantie d’être optimisée par le compilateur en une boucle itérative, évitant le débordement de pile. Le compilateur vérifie même la faisabilité de l’optimisation au moment de la compilation, et génère une erreur si les appels de queue ne sont pas possibles.

Google AdInline article slot

Pourquoi V8 ne prend-il pas en charge TCO ? Principales raisons :

  • Perte des traces de pile. TCO remplace le cadre de pile actuel, rendant le débogage plus difficile. En Scala, le compilateur transforme la récursion en boucle tout en préservant la pile d’appels.
  • Compatibilité arrière avec les API legacy. Des méthodes comme Function.caller dépendent de l’historique des appels, que TCO efface.
  • Complexité dans la compilation JIT. L’architecture multi-étapes de V8 (Ignition → TurboFan) nécessiterait une refonte de la génération et de l’invalidation des cadres.

Les ingénieurs de V8 affirment ouvertement que le coût de l’implémentation de TCO l’emporte sur les bénéfices pour l’écosystème. Ce choix reflète les priorités de JS : les fonctionnalités dynamiques et la compatibilité arrière priment sur les garanties fonctionnelles strictes.

Collections paresseuses et partage structurel : Sous-optimal pour les gros volumes de données

En JavaScript, les opérations sur les tableaux (filter, map) créent des copies intermédiaires, faisant exploser l’utilisation mémoire :

Google AdInline article slot
const result = hugeArray
  .filter(x => x > 0)   // [1] crée le tableau A
  .map(x => x * 2)      // [2] crée le tableau B
  .filter(x => x < 100) // [3] crée le tableau C
  .slice(0, 10);        // [4] crée result

Tous ces éléments peuvent coexister en mémoire — hugeArray, A, B, C — même si le résultat final est minuscule. Le ramasse-miettes s’exécute de manière asynchrone, donc le pic de charge est bien réel.

En Scala, les vues paresseuses (.view) transforment la chaîne en un pipeline à la demande sans collections intermédiaires. De plus, les structures persistantes comme Vector utilisent le partage structurel — seule la voie de la racine à la feuille est copiée lors d’un « changement » (O(log n)), le reste étant réutilisé.

JS ne prend pas cela en charge nativement. Les générateurs et les nouveaux Iterator Helpers (ES2025) permettent des chaînes paresseuses, mais ils nécessitent du code impératif et ne font pas partie de l’API standard. La raison ? L’optimisation pour les cas d’usage courants :

  • Localité des données. Les tableaux contigus s’accordent bien avec les caches CPU, tandis que les chaînes paresseuses mènent à de petits appels.
  • Optimisations JIT. V8 intègre agressivement les méthodes Array.prototype, ce que les opérations paresseuses empêchent.
  • Performances UI. Pour le rendu d’interfaces, copier des tableaux peut être plus rapide que la chasse aux pointeurs dans les arbres.

Gestion des erreurs : Exceptions vs. Résultats typés

En JavaScript, les erreurs sont des exceptions masquées au système de types. Par exemple, JSON.parse a la signature (string) => any, mais peut lever une exception :

function getUser(id: string): User {
  // Peut lever une exception — le compilateur ne prévient pas
}

Les exceptions rompent la transparence référentielle et transforment des fonctions totales en partielles. Les langages fonctionnels encodent les erreurs dans le type de retour :

val result: Try[Json] = Try(parse(userInput))

Ici, Try signale explicitement un échec potentiel, et le compilateur exige la gestion des deux cas. En JS, try/catch est une instruction, pas une expression, ce qui rompt le flux de données et complique la composition.

Des bibliothèques comme fp-ts offrent des palliatifs, mais ce sont des solutions externes, pas intégrées au langage. Cela crée des angles morts dans le système de types où les erreurs surgissent de manière inattendue.

Points clés pour les développeurs

  • JS/TS ne garantit pas l’immutabilité. Même const ne protège pas contre la mutation d’objets. Une vraie immutabilité nécessite des palliatifs (Immutable.js), ajoutant de la complexité au code.
  • La récursion est risquée en JS. Sans TCO, les algorithmes récursifs ne conviennent pas aux gros volumes de données. Vérifiez toujours la profondeur de récursion.
  • Les opérations paresseuses ne sont pas natives. Gérer de grands tableaux sans pics mémoire nécessite du code impératif ou des bibliothèques tierces.
  • Les erreurs sont des risques cachés. Les signatures de fonctions ne reflètent pas les exceptions. Enfermez toujours les opérations non fiables dans try/catch, même si TS ne l’exige pas.

— Editorial Team

Advertisement 728x90

Lire ensuite