Un dynamisme trompeur

Différences entre langages statiques et dynamiques

Ce deuxième billet, dont je conseille la lecture après celle du premier, tente de clarifier ce que distingue fondamentalement Python ou Javascript de C et C++. Et au passage, de séparer la différence de philosophies de programmation de celle de philosophies de traduction qui existe entre ces deux mondes informatiques. Pour s’attaquer à ce vaste programme, utilisons encore une fois une analogie naturelle particulièrement illustrative du problème.

Traduction et interprétation

Supposons que vous êtes un traducteur, dont la tâche est de traduire un discours entre deux langues différentes. Pour fixer les idées, le discours correspond au programme informatique, la langue de départ est un quelconque langage de programmation, et la langue de destination est l’assembleur; seul langage que l’ordinateur peut comprendre et donc exécuter. Il existe deux contextes bien distincts dans lesquels cette traduction peut s’effectuer, qui donnent lieu à deux métiers linguistiques différents.

Traduction : le traducteur classique va disposer d’un texte original en entier, et d’autant de temps que nécessaire pour le traduire. Il peut donc lire le texte une à plusieurs fois au préalable afin d’en analyser le style, et de déterminer une stratégie de traduction qui garantit la cohérence. Il se demandera ainsi comment traduire les noms propres, ou bien une expression récurrente utilisée dans différents contextes. Après cette analyse, il commencera à produire la traduction de manière plus ou moins linéaire mais parfois en plusieurs passes, chacune apportant son lot de corrections au texte dans la langue d’arrivée. Son but est d’obtenir sans trop de contraintes de temps un nouveau texte se suffisant à lui-même, de meilleur qualité possible.

Interprétation : l’interprète est quant à lui soumis à la contrainte du temps réel. Le discours est en train d’être prononcé; et son auditoire étranger attend impatiemment de savoir ce qu’il s’y dit. L’interprète n’a qu’une vision en fenêtre glissante sur le discours prononcé : sa mémoire immédiate ne lui permet de se souvenir qu’au plus de la dernière phrase prononcée, et surtout il n’a pas connaissance de la suite du discours. Dans cet état d’esprit complètement différent, la cohérence est difficilement maintenue au delà d’une phrase; le moindre temps passé à chercher un idiotisme adapté pour un expression se paye en retard sur le discours qui, lui, continue d’être prononcé. L’interprétation cherche donc à produire le plus vite possible un flux compréhensible dans le langage d’arrivée fidèle au discours d’origine.

Langages dynamiques et langages statiques

Revenons au monde de l’informatique. Dans la plupart des situations, le contexte de production et d’exécution des programmes correspond bien à de la traduction classique : le programme est traduit (compilé) une fois afin de produire un programme assembleur (exécutable) qui peut être exécuté par l’ordinateur, autant de fois que nécessaire. Ce contexte traditionnel est adapté à l’utilisation de langages dits statiques, terme qui décrit précisément l’étape de compilation préalable à l’exécution. Il existe néanmoins deux exceptions à ce processus, qui correspondent aux cas d’utilisation principaux de Javascript et Python.

Le premier est celui des navigateurs internet : lorsque vous chargez une page internet, le code Javascript qui va avec doit être exécuté le plus vite possible, afin d’animer les différents éléments qui s’affichent. On interprète dans ce cas car on ne peut pas imposer à l’utilisateur le temps d’une compilation en plus du temps de téléchargement. Nous reviendrons plus en détail plus tard sur le problème du navigateur car il est en fait plus subtil et extrêmement instructif.

Le deuxième cas est celui du développement informatique. En effet, lorsque l’on développe un programme, on suit très souvent un cycle : modification du code, exécution du programme, analyse des résultats, poursuite des modifications. Or, l’étape « exécution » est ralentie pour un langage statique car il faut rajouter ce temps mort de compilation du programme, pendant lequel le développeur ne reçoit aucune information pertinente à son débogage. À partir de là, le caractère interprété de Python en fait un candidat idéal pour les projets dont le mode de développement repose sur un cycle modification-exécution très court. Typiquement, le petit script prototype d’une centaine de lignes affectionné par les scientifiques des données. Python et Javascript sont donc qualifiés de dynamiques, parce qu’ils sont interprétés. Il est important de ne pas se laisser influencer par les connotations contraires de « statique » et « dynamique », car il ne s’agit en fait que de deux modes d’exécution différents adaptés chacun à des usages particuliers.

Un problème de types

Il nous faut maintenant aborder le délicat sujet des ces fameux systèmes de types sur lesquels les opinions sont parfois contrastées. Ce qui suit est plus aride que le reste, mais essentiel à la compréhension du cœur du problème. La métaphore linguistique nous sera encore d’un grand secours. Un programme typé est un texte dont on aurait annoté consciencieusement touts les mots de leurs catégories et fonctions grammaticales; par exemple :

<<Le>(article défini) <chien>(nom)>(groupe nominal, sujet)
<attrape>(verbe, présent)
<<la>(article défini) <balle>(nom)>(groupe nominal, COD)

Comme on peut le voir par analogie sur cette simple phrase, écrire un programme typé est plus pénible que d’écrire un programme non-typé. Mais quel est alors l’avantage de s’astreindre à une telle rigueur programmatique? La réponse est intimement lié au mode d’exécution du programme.

Lors d’une compilation, le compilateur (traducteur) va utiliser ces annotations de types pour effectuer la preuve d’un certain nombre de propriétés sur les variables du programme. Prenons un exemple : si le programme dicte a : int (lire a est de type int), b : int et print(a+b), alors le compilateur peut prouver que l’opération a+b est valide (car on peut additionner deux entiers), et que la fonction à utiliser pour afficher le résultat est la fonction qui affiche un int (résultat de l’addition). Le compilateur peut alors produire une suite d’instructions en assembleur qui réaliseront ce que spécifie le programme. Cette suite d’instructions constitue le programme qui pourra être exécuté par la suite.

Interprétons maintenant le même morceau de code. Rappelons que l’interprétation travaille sur des flux, ce qui implique que l’interpréteur va recevoir le programme symbole après symbole. Voici les étapes de l’interprétation.

  • a : int et b : int : on garde ces informations en mémoire.
  • a+b : c’est une addition, il faut donc vérifier que les deux arguments sont bien des entiers. L’interpréteur va donc émettre des instructions assembleurs dont l’effet est d’aller chercher en mémoire le type de a et b, et de le comparer au type attendu. Une fois ces instructions exécutés (immédiatement après être produites), l’interpréteur va émettre et exécuter l’instruction qui réalise l’addition.
  • print : l’interprète va aller chercher en mémoire le type de ce qu’il faut afficher, et en fonction du résultat charger la fonction permettant d’afficher ce type. Une fois ceci fait, il peut afficher le résultat.

On voit ici que l’interprétation conduit à d’innombrables vérifications (de l’ordre de quelques unes par instruction « réelle ») faites pendant l’exécution du programme. Dans le cas de la compilation, ces vérifications sont faites une fois pour toutes, et n’apparaissent pas dans le programme en assembleur généré. Si le programme n’est pas valide, le compilateur affiche une erreur et ne génère pas de programme assembleur. Au contraire, l’interpréteur soulève une erreur durant l’exécution.

La compilation d’un langage typé permet donc de prouver une partie de la correction d’un programme, ce qui permet d’identifier la plupart des bugs les plus évidents qui s’y trouveraient et éviter des erreurs à l’exécution. Or, dans un langage dynamique, l’annotation préalables des types est inutile car ceux-ci ne permettent pas d’éviter les vérifications et les erreurs à l’exécution. À partir de là, il n’y a pas d’intérêt pour le programmeur de fournir l’effort supplémentaire d’annotation des types. C’est pour cela que Javascript et Python ne sont pas typés.

Deux philosophies concurrentes

Après ce paragraphe, on peut se demander : quel est l’avantage d’utiliser un langage dynamique qui sera éternellement handicapé d’un point de vue de la performance par ces vérifications indispensables, et si nombreuses à l’exécution? D’abord, rappelons que les langages dynamiques sont essentiels dans les cas détaillés plus haut où l’interprétation est de rigueur. Mais en dehors de cela, on constate que ces langages sont de plus en plus utilisés dans des contextes statiques tels que du code de serveur (voir les exemples de Django ou Node.js).

Il est facile de prendre une posture de théoricien, indigné que la plèbe ne comprenne pas la supériorité mathématique du statique. Mais en réalité il faut prendre en compte des considérations psychologiques dont l’influence est considérable. Ce qui suit, ne se basant pas sur des faits ou données est une interprétation personnelle mais qui me semble plausible.

En effet, les langages statiques sont développés depuis maintenant 40 ans et la première génération de ces langages commence à sentir le passage du temps. Le C et le C++ qui restent les grands standards possèdent chacun un système de type, primitif pour le C, exubérant pour le C++, qui tous deux se tirent une balle dans le pied en exposant un type void qui peut être n’importe quoi, devenant ainsi une inépuisable source de bugs. D’autre part, la nouvelle vague des langages fortement typés (OCaml, Haskell, Rust) s’appuient sur des théories mieux conçues, mais qui donnent un côté « preuve » assez austère au programme qui peut rapidement repousser le développeur peu versé dans la mathématique. Dans ces langages fortement typés, le compilateur possède énormément d’informations et prouve beaucoup plus de choses sur le programme pendant son travail, exposant ainsi plus de bugs qui soulèvent autant d’erreurs. Néanmoins, il y a quelque chose de très frustrant de se battre contre le compilateur pendant le débogage, car on a le sentiment de n’être pas assez bon pour ne serait-ce qu’exécuter le programme. Le développeur tente alors de se raccrocher à sa bouée de sauvetage, les messages d’erreurs, mais il est très difficile d’en produire de bonne qualité lorsque les algorithmes de preuve dans le compilateur sont complexes. Tout cela impose donc une barrière à l’apprentissage assez élevée pour le développeur.

En contraste, le langage dynamique donne l’impression au développeur d’être en contrôle de son programme : lorsqu’une erreur se produit à l’exécution, il peut localiser sa provenance exacte (l’instruction à laquelle l’interprétation s’est arrêtée), utiliser des print, etc. L’absence de type permet également de jouer sur un polymorphisme (ce mot fera l’objet d’un article à part) de circonstance prompt au hack, ce qui donne un côté plus créatif au développement. Ainsi les langages dynamiques occupent un créneau intéressant dans l’espace des langages de programmation, car ils ont l’avantage de l’accessibilité, de la simplicité conceptuelle qui permet à l’utilisateur de comprendre ce qui se passe.

Conclusion

Si les langages statiques seraient, dans un monde idéal, l’outil privilégié du développeur professionnel dont le soucis est la performance et l’absence de bugs, un langage dynamique est un bon point de départ pour le développeur moins expérimenté. Utiliser un langage dynamique dans un contexte statique n’est pas un crime (du moins lorsque la performance n’est pas une caractéristique cruciale), et en effet ça fait le job, mais au prix de combien d’heures de débogage qui auraient pu être évitées par l’utilisation de types? Si commencer son prototype en Python est généralement un bon choix, une alarme interne doit retentir lorsque le projet dépasse le millier de lignes : est-ce que mon langage correspond à la portée et le contexte d’exécution de mon programme? Si la réponse est non, il devient rentable d’investir un peu de temps pour apprendre un langage typé (et donc statique). Néanmoins, le débat statique/dynamique reste un sujet clivant parmi les programmeurs et recèle d’autres subtilités que nous évoquerons dans un futur billet.

Pour aller plus loin