Interface fonctionnelle ou impérative

Les opérations d'un protocole peuvent être fonctionnelles (à savoir sans effet de bord) ou impératives (avec effets de bord). Le plus fréquemment, en programmation impérative (et en programmation orientée-objets), les opérations sont impératives. Mais Common Lisp n'est pas un langage purement impératif, ni purement orienté-objets. Il est donc souvent préférable de présenter une interface sans effet de bord. De plus, la mise au point (en Anglais : debugging) est plus facile avec des interfaces fonctionnelles.

Les opérations et de préférence le protocole entier, doivent être soit complètement impératifs, soit complètement fonctionnels. Une opération qui n'a pas naturellement d'effet de bord, peut figurer dans les deux types de protocoles. Un exemple est le test empty pour vérifier si un conteneur est vide. Cette opération n'a pas d'effet de bord, mais elle est bien sûr utile dans une interface impérative.

Si l'interface d'une opération est impérative avec effets de bord, l'opération doit laisser les objets passés en argument dans un état prévisible et documenté. Imaginons par exemple le code suivant :

* (defparameter *p* (make-thing ...))

*P*
* (transform-thing *p*)

...

Après l'opération transform-thing, la valeur de la variable *p* doit être un objet cohérent transformé selon la description de l'opération. Il est inacceptable de demander au programmeur consommateur de faire la chose suivante

* (defparameter *p* (make-thing ...))

*P*
* (setf *p* (transform-thing *p*))

...
car cette interface est (peut-être partiellement) fonctionnelle. L'interface de la fonction primitive sort est donc inacceptable en tant qu'opération d'un type abstrait, car cette fonction se permet de modifier son argument de manière destructive ; le programmeur ne dois pas s'en servir après l'appel et c'est la valeur de retour de sort qui contient l'objet transformé (la liste triée).

Ceci ne veut pas dire que la fonction sort soit mal écrite. Ça veut simplement dire que sort ne fait pas partie d'un protocole de type abstrait. Cette fonction est destinée à la manipulation de types concrets (ou types d'implémentation.

Pour l'exemple ci-dessus, il n'est pas non plus acceptable de définir transform-thing sous la forme de macro afin d'y cacher le setf dedans. Il y a plusieurs raisons à cela. La raison principale est que la situation suivante est problématique :

* (defparameter *p* (make-thing ...))

*P*
* (defparameter *q* *p*)

*Q*
* (transform-thing *p*)

...

Si transform-thing est une macro, il est possible de modifier la variable *p* dedans, mais la variable *q* contiendra toujours l'ancien objet détruit. Dans la situation ci-dessus, les deux variables doivent contenir le même objet (dans le sens de eq) après la transformation.

Les macros Common Lisp push et pop sont donc inacceptables en tant qu'interfaces d'un type abstrait. Encore, cela ne veut pas dire que ces macros sont mal écrites, mais simplement que leur but n'est pas de travailler sur un type abstrait, mais sur le type très concret liste.

L'utilisation d'une macro avec un setf caché dedans empêche aussi l'utilisation d'une expression arbitraire comme argument. L'exemple suivant devient impossible :

* (transform-thing (compute-and-save-a-thing))

...

De plus, écrire l'opération sous forme de macro empêche son utilisation dans des fonctions d'applications (voir section 8.7). L'utilisation de macros doit être réservée aux extensions syntaxiques.

Si l'interface de l'opération est fonctionnelle, il est au contraire interdit par l'implémentation de modifier les arguments. Un programmeur consommateur qui voit un code comme ceci :

* (defparameter *p* (make-thing ...))

*P*
* (setf *q* (transform-thing *p*))

...

doit pouvoir compter sur le fait que l'opération ne modifiera pas l'objet valeur de la variable *p*. À nouveau, la fonction sort est un exemple d'interface inacceptable pour un type abstrait.

Il n'est pas interdit qu'une opération d'une interface impérative renvoie une valeur. Mais le programmeur consommateur ne doit pas être obligé de récupérer cette valeur et l'intégrité des arguments doit toujours être respectée. Un exemple de cette situation est l'opération de dépilement d'une pile. Il est entièrement acceptable pour cette opération de supprimer l'élément sommet de pile et de renvoyer l'élément ainsi supprimé.

Irene DURAND
2011-10-14