Typologie des langages de programmation

I. Introduction

Il existe des milliers de langages de programmation. On considère d'ailleurs que plus de 1500 langages sont utilisés à relativement large échelle de par le monde. Il existe de nombreuses raisons justifiant l'apparition de nouveaux langages, et le choix de tel ou tel langage pour répondre à un problème.

Dans la suite de ce document, quelques grands critères permettant de comparer les langages de programmation sont présentés.

II. Paradigmes fondamentaux du langage

On appelle paradigme de programmation la façon dont on envisage l'exécution d'un programme. On distingue souvent 3 paradigmes principaux de programmation

Programmation impérative

La programmation impérative est une programmation dans laquelle un programme est fait d'instructions dont l'exécution modifie l'état du programme. C'est le paradigme sous-jacent à la plupart des langages de programmation. Même si la plupart des langages impératifs sont des langages où les instructions sont exécutées séquentiellement, ce n'est nullement une obligation ; par exemple, dans un langage comme ADA, on peut spécifier que des instructions seront exécutées en parallèle. Les premiers langages de programmation étaient des langages impératifs. De nombreux langages récents sont également des langages impératifs.

Quelques exemples : C, Python, Java, Ada, Javascript

Programmation fonctionnelle

La programmation fonctionnelle est une programmation dans laquelle un programme est une composition de fonctions calculant un résultat à partir de données d'entrée. La théorie sous-jacente est celle du λ-calcul, introduite par Church en 1925. Le premier langage fonctionnel fut le Lisp (1958), qui eut un certain succès, notamment dans le domaine de l'IA. D'autres langages fonctionnels sont à citer :

-- Exemple de programme en Haskell
-- lancer ghci puis taper :load peano

data Peano = Zero | Succ Peano deriving Show

addition Zero x = x
addition (Succ entier1) entier2 = addition entier1 (Succ entier2)

(+.) Zero x = x
(+.) (Succ entier1) entier2 = entier1  +. (Succ entier2)

conversion Zero = 0
conversion (Succ entier) = 1 + conversion entier

multiplication Zero _ = Zero
multiplication (Succ entier1) entier2 = addition entier2 (multiplication entier1 entier2)

conv2 0 = Zero
conv2 x = Succ (conv2 (x-1))

Le paradigme fonctionnel a le vent en poupe : de nombreux langages récents intègrent des aspects fonctionnels, au sens où :

Parmi ces langages, on trouve notamment :

Programmation logique

La programmation logique est une programmation déclarative dans laquelle un problème va être décrit par des faits et des règles logiques. La résolution du problème consistera à trouver des valeurs satisfaisantes pour les valeurs inconnues en utilsant essentiellement 2 mécanismes : l'unification et le back-tracking (retour en arrière dans le cas de la récursivité). Les langages logiques sont en général réservés à la résolution de problèmes, et donc à l'intelligence artificielle. Le langage principal mettant en oeuvre ce paradigme est le langage Prolog, créé à Marseille par Alain Colmerauer en 1972.

# Exemple de programme en Prolog
# lancer swipl puis taper [peano].

entier(0).
entier(s(N)):- entier(N).

addition(N, 0, N).
addition(s(NM), s(N), M):-addition(NM, N,M).

multiplication(0, 0, _).
multiplication(Produit, s(N), M):- multiplication(ProdNM, N, M), addition(Produit, M, ProdNM).

conversion(0,0).
conversion(Entier,s(P)):- conversion(EntierP, P), Entier is EntierP+1.

III. Paradigmes secondaires

Certaines autres caractéristiques des langages de programmation sont quelques fois considérés comme des paradigmes. En voici quelques-uns :

Programmation orientée objets

Les langages orientés objets sont des langages dans lesquels les structures de données définissent aussi les traitements qui s'y rattachent. Si la plupart des langages orientés objets sont des langages de classe, certains reposent sur d'autres modèles, comme Javascript, qui est un langage à prototype.

La plupart des langages orientés objets sont des langages impératifs, mais OCAML (extension de CAML), par exemple, est un langage fonctionnel orienté objets

Programmation événementielle

La programmation événementielle est un style de programmation qui consiste à déclencher des traitements lorsque des événements (externes ou générés par d'autres parties du programme) surviennent. C'est un style de programmation souvent utilisé dans le développement d'interfaces graphiques (entre autres dans le développement de documents Web). C'est aussi la base du fonctionnement d'un langage comme Scratch par exemple.

Programmation orientée aspect

La programmation orientée aspect consiste à spécifier isolément certains aspects transverses au code métier afin d'éviter de le disséminer dans tous les fichiers source. Du coup, un "tisseur" d'aspects (programme qui ré-introduit ensuite les aspects aux bons endroits dans le code source avant compilation ou exécution) est nécessaire.

IV. Classification selon le code exécuté

Selon les langages, le code source peut être interprété directement lors de l'exécution, ou bien ce peut être une traduction de ce code source dans un autre langage qui est exécutée.

Langage interprété

Un langage interprété est un langage dont le code exécuté est le code source. Comme celui-ci n'est en général pas compréhensible par la machine, un interprète doit jouer le rôle de traducteur à la volée pour chaque exécution du programme. Un langage interprété est en général d'utilisation aisée, mais est lent à l'exécution, gourmand en ressources, et laisse souvent subsister des bugs.

Quelques exemples :

Langage compilé

Un langage compilé est un langage dont le code source est traduit une fois pour toute dans le langage du micro-processeur (langage machine) par un programme appelé compilateur. Le fichier ainsi produit est souvent appelé "code objet". L'utilisation d'un langage compilé est a priori moins facile que celle d'un langage interprété, puisque la phase de compilation est indispensable entre une modification du programme et son exécution. Il y a cependant 2 avantages essentiels par rapport aux langages interprétés :

Quelques exemples :

Langage semi-compilé

Un langage semi-compilé est un langage dont le code source doit être traduit par un compilateur dans un code intermédiaire, proche du langage machine, appelé souvent byte code. Pour l'exécution, un interprèteur doit exécuter ce code intermédiaire.

Le code intermédiaire étant très proche d'un langage machine, son interprétation est très rapide. L'exécution est donc bien plus rapide que celle d'un langage interprété. Par ailleurs, le code intermédiaire est générique, donc la compilation ne doit pas être faite pour chaque architecture cible.

La notion de bytecode est apparue en 1972. Le langage qui a commencé à la populariser est sans nul doute le langage Java.

Compilation Juste à temps (JIT)

De plus en plus de langages interprétés ou semi-compilés proposent des compilations "juste à temps" : à l'exécution, si l'interprèteur détecte qu'un morceau du code est beaucoup utilisé, il peut dynamiquement compilé ce morceau là afin que les exécutions ultérieures soient beaucoup plus rapides. Cette pratique est notamment mise en oeuvre dans des langages comme Python ou Java.

V. Niveau du langage

Il est également possible de classé les langages selon leur "niveau" : plus un langage est de haut niveau, plus il est éloigné de l'architecture matériel.

Langage de bas niveau

Font partie de cette catégorie les assembleurs : ce sont des langages qui permettent juste de simplifier l'utilisation du langage machine : on utilise des mots-clés pour les instructions plutôt qu'un code hexadécimal, et on peut utiliser des étiquettes pour éviter des calculs d'adresse

Langage intermédaire

On considère un langage comme un langage intermédiaire si celui-ci présente des structures de contrôle de plutôt haut niveau mais que malgré tout, la représentation en machine des données et du programme doivent être relativement bien connues pour pouvoir maîtriser le langage.

Le langage C est un exemple typique de langage intermédiaire.

Langage de haut niveau

Les langages de haut niveau constitue la majeure partie des langages de programmation. Python, Java, Javascript, etc. font partie de cette catégorie.

Langage de 4ème génération

Les langages de quatrième génération sont des langages proposés par une application pour permettre d'automatiser des traitements de l'application. VBA en est un exemple.

VI. Langage et typage

A. Du typage strict au typage faible

1. Les différentes catégories de typage

Un langage de programmation manipule des données. Ces données sont des valeurs d'un certain type. D'une façon plus formelle, on peut voir un type comme un ensemble et une valeur comme un élément de cette ensemble. Ainsi, quand un programme manipule la donnée "3.14", il s'agit d'un élément de l'ensemble des réels.

Dans un langage à typage strict, les types sont en fait des "classes" au sens de la théorie des ensembles de Zermelo-Fraenkel (et non au sens des langages à base de classes), ce qui signifie qu'ils sont disjoints et "non mélangeables" (ceci permet de rendre la théorie des ensembles consistante, en évitant par exemple le paradoxe de Russel). Par ailleurs, toute variable est aussi caractérisée par un type : une variable de type t ne peut contenir que des données de type t.

Un langage à typage strict est un langage particulièrement sûr, dans la mesure où cela va empêcher d'additionner des choux et des carottes. Mais cela rend le langage difficilement utilisable en pratique, puisque dans un tel langage, les nombres 2 et 2.0 n'ont rien à voir. Aussi, le nombre de langages à typage strict est-il proche de 0.

Beaucoup de langages ont relâché certaines contraintes en autorisant quelques conversions de type implicites. On appelle ces langages des langages fortement typés. Dans ces langages, les conversions entier → flottant sont par exemple implicites. Mais cela ne va guère plus loin. Or du contexte numérique, il n'y a pas de conversion implicite (les booléens et les nombres restent distincts, par exemple). Par ailleurs, les variables sont toujours des variables typées, et leur type doit être déclaré où inferrable. Le langage Java est un exemple de langage fortement typé.

Les langages fonctionnels ont également en général des langages fortement typés, mais la plupart du temps, il n'y a pas besoin de déclarer le type des variables car celui-ci peut être inféré par l'interpréteur ou le compilateur.

D'autres langages permettent de lever plus ou moins ces contraintes. On parle alors de langages faiblement typés. Par exemple, dans le langage C, il n'y a pas de type "booléen" : dans un contexte booléen, un entier non nul est interprété comme vrai, tandis que 0 est un interprété comme faux.

Python est un exemple de langage faiblement typé, dans la mesure où, par exemple, les variables ne sont pas typées. On peut ainsi écrire : a = 2 ; a = "toto". Malgré tout, il y a peu de conversions implicites de types en Python, et utiliser une variable non initialisée lève une exception (contrairement à ce qui se passe en C par exemple).

Certains langages sont très faiblement typés. C'est le cas par exemple de Javascript, qui met en oeuvre un nombre important de mécanismes de conversion implicite. Cela rend plus rapide l'écriture du code, mais va au détriment de la sûreté de fonctionnement.

2. En guise de conclusion

De façon générale, plus un langage est fortement typé, moins le risque de bugs est important. Par ailleurs, un langage fortement typé oblige à une certaine rigueur dans la tâche de programmation, et cela ne peut qu'être judicieux pour l'apprentissage de la programmation.

D'un autre côté, un langage faiblement typé est souvent moins verbeux, donc permet d'écrire du code plus rapidement. Mais cela se fait au détriment de la sûreté de fonctionnement.

En conséquence, les langages faiblement typés devraient être réservés à du prototypage ou à l'écriture de petits programmes. Par contre, dans un contexte pédagogique ou dans le cadre d'un projet conséquent, il ne faudrait utiliser que des langages fortement typés.

B. Langages objets et typage

Dans les langages à base de classe, les classes sont souvent opposées aux types primitifs. C'est par exemple le cas de Java ou de C++ (même si cela est critiqué par les partisans des langages objets "forts", dans lesquels tout est objet).

Dans ces langages, tous les objets font partie d'une même "classe" (au sens de la théorie des ensembles de Zermelo-Fraenkel), que l'on peut assimiler au type Object de Java. Chaque classe définit un sous-ensemble de ce type. Définir une classe B comme sous-classe de A revient à définir l'ensemble des objets de type B comme un sous-ensemble de l'ensemble des objets de type A.

Une fois ces concepts acquis, il devient logique de comprendre que si B hérite de A, alors une variable a de type A peut contenir une donnée de type B,mais qu'une variable b de type B ne peut a priori pas contenir une donnée de type A.

Ainsi, le code suivant est correct :

  //La classe B hérite de A
  A a;
  B b;
  ...
  a = b;

Mais le code qui suit est erroné :

  //La classe B hérite de A
  A a;
  B b;
  ...
  b = a;