Ce que les paradigmes de programmation disent vraiment

En théorie, tous les langages de programmations sont équivalents. Les problèmes qu’on peut résoudre avec l’un peuvent être résolut avec un autre. En particulier parce qu’ils peuvent se simuler les uns les autres (on dit alors qu’ils sont Turing Complets) et pour s’en convaincre, il suffit de se rappeler qu’on exécute tous les programmes sur un processeur et qu’on peut l’émuler par des programmes.

Mais en pratique, ils sont bien différents. Il y a d’abord le vocabulaire et la grammaire qui les rendent plus faciles à utiliser (ou pas). Il y a ensuite les fonctionnalités offertes par défaut et qui faciliterons la résolution des problèmes (ou pas). Sans oublier l’écosystème de modules additionnels et la communauté humaine qui apporteront un soutien bienvenue (ou pas).

Face à la multitude des langages, le mythe voudrait qu’on fasse le choix rationnellement. En pesant le pour et le contre, qu’on détermine le meilleur d’entre eux. Les enseignant les plus ouverts admettront que ce choix dépend du contexte mais, dans l’idée, il y a toujours un langage supérieur aux autres dont les missionnaires se feront une joie de vous en convaincre.

Ce choix est en fait politique. Choisir un langage, c’est choisir les paradigmes qu’il met en œuvre. Déclaratif, impératif, fonctionnel, orienté objet, ... C’est définir ce que programmer veut dire. Et ça a des conséquences.

Programmation Déclarative

La programmation déclarative consiste à énoncer l’objectif qu’on souhaite obtenir et laisser le système résoudre le problème à notre place.

Par exemple, en HTML, on peut écrire un document dont le contenu stipule une page contenant un paragraphe dont le message est « hello world ». Charge ensuite au navigateur de résoudre les contraintes graphiques pour produire l’affichage correspondant.

<html>
    <body>
        <p>Hello World !</p>
    </body>
</html>

On retrouve cette façon d’aborder les problèmes dans d’autres domaines comme la programmation logique par contraintes. Voici un exemple en Answer Set Programming qui décrit un monde où world est une personne, hello est une salutation puis exprime comment sont construits les messages (une salutation suivie d’une personne).

person(world).
greets(hello).
message(S, P) :- greets(S), person(P).
#show message/2.

Ce programme peut alors être fourni à un solver qui se charge d’interpréter les contraintes et nous fournir un modèle qui les respectes, ici les messages possibles, c’est à dire « hello world ».

clingo version 5.7.2 (6bd7584d)
[...]
Answer: 1
message(hello,world)
SATISFIABLE
[...]

De manière encore plus générale, les chat bots peuvent être vu comme une programmation déclarative. On exprime le résultat qu’on voudrait et ils se chargent de le produire... avec plus ou moins de réussite (ici il n’a pas respecté la casse).

Programmation déclarative avec ChatGPT

Dans ce genre de situation, on ne parle pas vraiment de programmation. Parce qu’il s’agit principalement d’émettre des contraintes et de laisser l’outil trouver une solution qui les respecte. On pourrait l’assimiler à prier une entité supérieure pour qu’elle résolve le problème. Si l’entité est efficace, et si le problème est dans sa zone de compétence, la solution sera adaptée. Mettre en forme un document multimédia, trouver un modèle logique, générer du texte qui ressemble à du texte. Et sinon…

Programmation Impérative

À l’opposé, la programmation impérative consiste à fournir des instructions exactes à une machine qui va les exécuter telles quelles et fournir le résultat correspondant (aux opérations ; il n’est pas toujours correct).

Par exemple, on pourrait écrire un programme qui affiche « hello world » sur la sortie standard en assembleur x86-64 comme suit. On défini une zone mémoire contenant le message puis, dans le code, on prépare les registres et on exécute l’appel système write(). Le code C équivalent est commenté en vis à vis pour vous montrer que l’idée est assez générale.

hello: .string "hello world\n"    # char hello = "..." ;
       sizeofhello = . - hello
/* ... */
write:
    mov $0x01,        %rax        # write(
    mov $0x01,        %rdi        #     stdout,
    lea hello,        %rsi        #     hello,
    mov $sizeofhello, %rdx        #     sizeof(hello),
    syscall                       # ) ;

Ce style de programmation convient surtout pour des scripts. Plutôt que de refaire le même enchaînement de commandes, encore et encore, on les écrits dans un fichier et c’est lui qu’on fournira au système pour qu’il les exécute automatiquement. Voici un exemple en bash :

echo "hello world"

Ce n’est pas flagrant sur un hello world mais l’idée de ce style de programmation, c’est surtout d’automatiser des tâches qu’on ne veut (ou qu’on ne peut) pas faire soi-même. Il n’y a pas d’ambition à créer un Grand Programme qui résolve un Grand Problème. Le but c’est de résoudre des petites tâches de la vie courante : de la configuration système, de la résolution de défis en ligne, des shellcodes, …

Pour passer à l’Ordre Supérieur, il faudra structurer tout ça. On le fait en regroupant des parties du programmes en morceaux autonomes. Fonctions, méthodes, … les noms ne manquent pas et ils correspondent au paradigme suivi pour gérer la complexité du problème.

Programmation Fonctionnelle

La programmation fonctionnelle est très proche des mathématiques. Le programme est vu comme un ensemble de fonctions qui prennent des paramètres et fournissent des résultats. Chaque fonction peut en appeler d’autres (voir s’appeler elle-même). Programmer consiste à écrire ces fonctions mathématiques et les appeler dans le bon ordre avec les bons paramètres.

L’extrait de code OCaml suivant en est un exemple. La première ligne défini une fonction salutation qui prend une personne en paramètre et affiche un message de bienvenue. La deuxième ligne défini la fonction principale (exécutée lorsque le programme est lancé) qui appelle salutation en lui fournissant son paramètre (le nom de la personne à saluer).

let salutation personne = print_endline ("hello " ^ personne) ;;
let ()                  = salutation "world" ;;

Techniquement, les puristes parleront plutôt de procédure. Un peu parce que salutation ne fourni pas de valeur de retour. Mais surtout parce qu’elle a un effet de bord. Et une Fonction mathématique respectable ne s’adonne pas à ces vulgarités.

Plein d’autres langages fonctionnels existent mais on peut retrouver ce paradigme dans des langages qui ne s’en prétendent pas forcément. L’extrait suivant est en JavaScript et suit la même logique. Une fonction pour saluer et une autre qui s’exécute une fois le document affiché. Petite variation, on a cette fois utilisé une structure pour transmettre l’entité à saluer (et plus une simple chaîne de caractère).

function salute(person) {
    console.log("bonjour " + person.name) ;
}

window.onload = function () {
    w = { name : "world" } ;
    salute(w) ;
}

L’avantage de ce tic de langage, à voir tout comme des fonctions, est qu’on peut leur appliquer des raisonnements mathématiques et, dans certaines limites, prouver quelques propriétés. Tout s’effondre lorsqu’on ajoute des effets de bords et ça explique l’animosité des puristes envers les procédures : elles violent les limites en sortant de leurs portées.

Il faut également avoir en tête que ce paradigme introduit une hiérarchie entre les fonctions. Les principales (qui savent) appellent leurs subordonnées (qui exécutent). On trouve parfois des fonctions qui s’appellent elle-même (une forme d’onanisme) voir mutuellement mais ce deuxième cas est rare et nécessite une syntaxe spécifique.

De même, les structures de données sont totalement transparentes pour les fonctions. Les fonctions savent ce qu’il faut faire et utilisent le contenu des structures pour mettre en œuvre la solution. Seul un système ad hoc de découpage en module permet (parfois) de réserver cet accès à certaines fonctions mais ce n’est pas naturel dans ce paradigme.

Programmation Orientée Objets

La programmation orientée objet propose d’inverser ce point de vue ; de délaisser les fonctions pour mettre l’accent sur les données. Les fonctions font alors partie intégrante des données qu’elles manipulent et l’ensemble produit un objet autonome. Programmer en POO consiste à définir et donner vie à un écosystème d’agents qui vont collaborer pour résoudre le problème.

Prenons un exemple en PHP. Cette fois nous définissons une classe qui décrit des personnes. Un attribut $name pour stocker le nom (et garder son accès privé), une première méthode pour construire ces objets (en fournissant un nom) et une autre pour les saluer. Les deux dernières lignes de code instancient un objet puis invoquent sa méthode.

class Person {
    private $name ;
    public function __construct($name) {
        $this->name = $name ;
    }
    public function salute() {
        echo "hello " . $this->name . "\n" ;
    }
}

$p = new Person("world") ;
$p->salute() ;

Au delà des droits d’accès sur les données, le réel intérêt de la programmation objet est qu’on peut définir des objets équivalents et les substituer les uns aux autres. Concrètement, ces objets doivent disposer de méthodes en commun (même nom, mêmes paramètres et, suivants les langages, quelques indications supplémentaires). Un code qui utilise ces méthodes peut alors utiliser n’importe lequel de ces objets en fonction des situations sans changement de son code à lui.

La hiérarchie se déplace des fonctions aux objets et ne traduit plus une autorité mais un héritage. Les objets parents lèguent leurs données et leurs comportements à leurs enfants qui peuvent, au besoin, les adapter et les étendre.

De manière générale, une bonne conception repose sur des structures opaques. Plutôt qu’accéder directement aux données d’un objet, il est plus efficace de masquer les données derrière des méthodes. Dit autrement, c’est l’objet lui-même qui sait comment gérer ses propres données.

La liberté des uns s’arrête où commence celle des autres.

Devise de la paix de Fexhe

Et après

Tels qu’on les rencontre la plupart du temps, ces paradigmes de programmation ne font que traduire un point de vue sur ce que programmer veut dire. La programmation déclarative se concentre sur le résultat à obtenir et l’impérative sur le moyen de l’obtenir. La programmation fonctionnelle utilise des fonctions omnipotentes et la programmation orientée objet crée un écosystème d’agents.

Plus qu’une façon de programmer, ces paradigmes définissent notre approche pour résoudre des problèmes... À force d'utiliser un marteau, le monde ressemble à des clous. Les outils qu'on utilise façonnent notre cognition, nos réflexes de pensées, nos façons d'envisager et résoudre les problèmes.

Quand on verse de l'eau dans une cruche, elle prend la forme de la cruche et elle n'est plus la même. Il y a une heure, ils n'imaginaient même pas qu'on puisse porter des noms, et maintenant ils se les disputent.

Terry Pratchett, Procrastination (p. 266), trad. Patrick Coutton

La guerres qui oppose les partisans de programmation fonctionnelle (Lisp, OCaml, Haskell) à la programmation orientée objet (Java, C++, ...) est bien plus profonde qu’une préférence entre les fonctions ou les données. Elle prend sa source dans notre rapport au monde.

On retrouve cette différence de point de vue dans bien d’autres domaines de l’informatique. La répartition client/serveur induit une asymétrie de rôle et de pouvoir entre le serveur central et les clients périphériques. Au contraire, la répartition pair à pair induit un équilibre et une convergence collective vers le but du service.

Toute organisation qui conçoit un système, au sens large, concevra une structure qui sera la copie de la structure de communication de l’organisation.

Loi de Conway

Si vous vous demandiez pourquoi les classes préparatoires informatiques (MP2I) enseignent la programmation fonctionnelle, pourquoi le concours de l’agrégation ignore tant de la programmation orientée objet ou encore pourquoi l’HADOPI cible le pair à pair, vous avez maintenant la réponse...