Le Common Lisp Object System, souvent abrégé en CLOS (prononcer « si-lauss »), est l'ensemble des primitives présentes dans le langage de programmationCommon Lisp pour construire un programme orienté objet. Il existe également une version de CLOS pour le langage Scheme, nommée TinyClos.
Présentation
CLOS est un système objet à classes (il existe des systèmes à prototypes). Il descend de Flavors et Common Loops, développés dans les années 1980. Common Lisp a été le premier langage à objet standardisé par l'ANSI, en décembre 1994, précédant de peu Ada 95 (), SmallTalk et C++ (tous deux en 1998).
Les objets et Lisp
D'un certain point de vue, Lisp est un langage orienté objet depuis le début : les structures de données manipulées ont une identité et sont réflexives. La seule chose qui lui manquait pour recevoir l'estampille orienté objet, est la capacité d'étendre le système de types de Lisp.
CLOS est construit autour de deux axes : les classes et les fonctions génériques.
Les classes de CLOS sont relativement similaires à celles des autres langages à objet. Ce sont des groupes d'attributs. La spécialisation par héritage multiple est permise. Il est possible de définir des accesseurs et des mutateurs particuliers pour chaque attribut.
Contrairement à la plupart des systèmes à objets classiques, les méthodes ne sont pas définies dans l'espace de nom des classes. Elles appartiennent à des fonctions génériques, qui sont des objets distincts. Cela permet de réduire le couplage a priori entre types de données et opérations sur ces types. Cela autorise naturellement l'extension de la sélection dynamique de méthode à l'ensemble des arguments obligatoires, et non pas au premier paramètre privilégié (appelé généralement le « receveur » de l'appel de méthode). Expliquons ces deux points.
Fonctions génériques
Des classes « conteneurs » peuvent être définies dans des bibliothèques ou des frameworks indépendants, et cependant, certains concepts génériques s'appliquent à toutes ces classes : nombre d'éléments, représentation sérialisée pour en prendre deux. Ces concepts génériques sont, dans des langages comme Java ou C++, obligatoirement fournis par héritage (interface en Java, mixin en C++), pour satisfaire le système de types statique. Si les concepteurs ont oublié de fournir une interface pour une telle fonctionnalité générique, il n'y a rien à faire (en pratique, on doit contourner le problème par des procédés alourdissant le code, par exemple écrire ses propres sous-classes du framework disposant de la fonctionnalité voulue, mais ce n'est pas toujours possible). Des langages dynamiques comme Python, permettent le « monkey-patching », c'est-à-dire l'ajout post hoc de méthodes à des classes existantes ; mais c'est une pratique déconseillée et posant des problèmes de maintenabilité.
La solution de CLOS à ce problème est la notion de fonction générique indépendante du site de définition et de l'espace de nom d'une classe associée. Ainsi on peut définir une fonction nb-elements disposant d'implémentations (méthodes) variées pour des classes de provenances diverses sans avoir à toucher à la définition même de ces classes.
Sélection multiple
Puisque les méthodes sont groupées dans des fonctions génériques, il n'y a aucune raison de considérer que le premier argument est « privilégié » (comme le self ou this d'autres langages à objets). On peut admettre des définitions de méthodes pour le tuple des types des arguments obligatoires.
Exemple avec des types numériques. L'opération d'addition, add pourrait être définie classiquement comme ceci (en pseudo-code) :
class int:
defmethod add(self, y):
if isinstance(y, int):
...
elif isinstance(y, float):
...
Avec seulement deux types numériques int et float, il faut donc définir deux classes (int, float) et deux méthodes. De plus, chaque méthode contient un test de type sur le second argument. Cela pose un problème d'extensibilité. Si l'on veut ajouter de nouvelles méthodes, il faut disposer du source original. Si l'on veut ajouter de nouveaux types numériques (rationnels, complexes ou autres), nous ne pouvons facilement étendre les méthodes existantes (sans, à nouveau, accéder au source et ajouter des branches aux tests de types).
En dissociant classes et méthodes, on se permet donc d'ajouter après coup autant de classes que nécessaires, de définir des méthodes non prévues initialement, et finalement de fournir de nouvelles implémentations de méthodes (fonctions génériques en fait) après coup, de façon non intrusive.