Les fermetures (+ notes sur la compilation de programmes Scheme)

Situation 

A partir d'un constructeur de vecteur 3D (vec3d x y z) et des accesseurs (xcor V), (ycor V) et (zcor V), définir une fonction (z-rotation alpha) construisant la fonction de rotation d'angle alpha autour de l'axe Oz.

Une solution :

     (define (z-rotation a)
       (let ((cosa (cos a))
             (sina (sin a)))
         (lambda (v)
           (let ((x (xcor v))
                 (y (ycor v)))
             (vec3d (- (* x cosa) (* y sina))
                    (+ (* x sina) (* y cosa))
                    (zcor v))))))

Utilisation :

Déclaration d'une fonction de rotation réutilisable, puis utilisation de celle-ci :
     (define z-rotation-pi/2 (z-rotation (/ pi 2)))
     (z-rotation-pi/2 (vec3d 1 0 2))
Ou, "à la volée" :
     ((z-rotation (/ pi 2)) (vec3d 1 0 2))

Comment cela fonctionne-t-il ?

La première définition, (define (z-rotation a) ...), associe au symbole z-rotation une lambda à un argument a.
La seconde définition, (define z-rotation-pi/2 (z-rotation (/ pi 2))), associe au symbole z-rotation-pi/2 le résultat de l'évaluation de (z-rotation (/ pi 2)), qui est donc une lambda à un argument, mais qui "sait" qu'au symbole cosa est associé le résultat de l'évaluation de (cos (/ pi 2)), et qu'au symbole sina est associé le résultat de l'évaluation de (sin (/ pi 2)) !
En fait, le symbole z-rotation-pi/2 est associé à un peu plus qu'une lambda : une fermeture (closure), qui correspond au couple <environnement,lambda>, la lambda étant alors évaluée dans cet environnement.
Dans notre cas, nous pourrions avoir le schéma suivant :

Closure diagram

C'est pas un peu lourd à évaluer, tout ça ?

Donc, une fois que l'on a écrit les 9 lignes permettant de définir z-rotation, nous pouvons créer autant de fonctions de rotation que nécessaire, et qui ne font qu'une seule ligne. Notons par ailleurs que cela n'a requis qu'une seule ligne supplémentaire ((lambda (v) au lieu de faire dès le départ un (define (z-rotation a v)).
L'utilisation de l'ordre supérieur (une fonction construisant une fonction) nous permet donc d'obtenir un code à la fois plus conçis (inutile de répéter plusieurs fois les mêmes angles), qui évite les recalculs (comme nous allons le voir, les sinus et cosinus sont censés n'être calculés qu'une seule fois pour un nombre quelconque d'applications), qui est plus lisible (les rotations peuvent être nommées, les expressions effectuant les rotations sont plus compactes), etc., soit un certain nombre de qualités essentielles distinguant le "bon code" de la lie commune, en grande partie responsable du fait que la plupart des logiciels ne fonctionnent pas complètement comme ils le devraient, mais là on s'éloigne du sujet...
Quoi qu'il en soit et au-delà de toutes les qualités que semble apporter la programmation d'ordre supérieur, on est en droit de se demander si toutes ces histoires d'environnements et autres fermetures ne rendent pas les programmes excessivement lourds à manipuler, autrement dit si les programmes rédigés ainsi n'en deviennent pas intrinsèquement lents.

Avant de tenter de répondre à cette question, il convient de distinguer entre interprétation d'un programme, et exécution d'un programme compilé.
En TP, nous travaillons en mode interactif, c'est à dire que nous rédigeons de nouvelles définitions, nous les testons en demandant l'évaluation d'expressions, nous raffinons les définitions, etc... Ce que l'on souhaite en premier lieu, c'est que le temps de réaction de l'environnement soit le plus faible possible : on ne veux pas avoir à attendre trop longtemps que les mises à jour du programme soient intégrées. Nous interagissons en fait avec un interprète qui, grossièrement, traduit les instructions du programme source à chaque fois qu'il les rencontre (soit potentiellement un grand nombre de fois, par exemple dans le cas de boucles), cela évitant une longue analyse initiale du programme.
Inversement, une fois le programme mis au point et que l'on génère une application destinée à être exécutée en dehors de l'environnement, on désire généralement qu'elle s'exécute le plus rapidemment possible, en consommant le moins de mémoire possible : on procède alors à la compilation de l'application. Un des principes de base est de minimiser les opérations à effectuer lors de l'exécution (at run-time) et de les effecuter lors de la compilation (at compile-time).
En résumé, on accorde à un compilateur le temps nécessaire à certaines analyses du code permettant de produire une application plus rapide et économe en mémoire, alors qu'à l'inverse on demande à un interprète d'être disponible rapidement, donc au détriment des analyses qu'il pourrait effectuer.

Ceci étant précisé, qu'est-ce qu'un bon compilateur de Scheme serait censé être capable de faire lorsqu'il voit la déclaration précédemment vue (define z-rotation-pi/2 (z-rotation (/ pi 2))) ?
Tout d'abord, z-rotation n'étant pas une forme spéciale, son argument (/ pi 2) devra être évalué avant l'application de la fonction. Mais, pi étant un symbole dont on connaît la valeur lors de la compilation, ce calcul peut être effectué à la volée.
On considère maintenant le corps de z-rotation, dans lequel on a remplaçé les références à l'argument par la valeur de (/ pi 2) : de la même manière, les valeurs associées aux symboles cosa et sina peuvent être calculées lors de la compilation.
Dès lors, il n'est plus nécessaire de construire un environnement spécifique pour pouvoir évaluer la lambda : il suffit de remplacer les références aux variables locales par leur valeur !
A ce stade, la définition de z-rotation-pi/2 revient à avoir écrit :
     (define (z-rotation-pi/2 v)
       (let ((x (xcor v))
             (y (ycor v)))
         (vec3d (- (* x 0) (* y 1))
                (+ (* x 1) (* y 0))
                (zcor v))))
De là, on ne s'arrête pas en si bon chemin : on simplifie les opérations impliquant 0 ou 1, et l'on arrive à :
     (define (z-rotation-pi/2 v)
       (let ((x (xcor v))
             (y (ycor v)))
         (vec3d (- y)
                x
                (zcor v))))
Enfin, on évite les déclarations de variables locales n'étant référençées qu'une seule fois, pour terminer avec :
     (define (z-rotation-pi/2 v)
       (vec3d (- (ycor v)) (xcor v) (zcor v)))

C'est pas yoli, ça ? Braaaaaavoooo !
Allez donc essayer d'obtenir un résultat similaire avec si peu de code (et aussi lisible !) en Pascal/C/C++/Java/whatnot...

Bon, bien évidemment, toutes ces explications sont un peu grossières, et il ne faudrait surtout pas s'imaginer que la compilation de programmes Scheme (ou autres) soit si triviale que ça. Quoi qu'il en soit, si ce sujet vous intéresse, alors vous prendrez beaucoup de plaisir à poursuivre vos études à la fac...

Où trouver un compilateur Scheme ?

La période des mini-projets approchant, vous aimeriez peut-être avoir accés à un bon compilateur Scheme, pour créer vos propres applications en Scheme, que l'on pourrait lancer sans environnement comme celui que nous utilisons en TP ?
Et bien le plus simple d'accès pour vous est mzc, disponible avec DrScheme, qui compile un programme Scheme en language C (un language impératif d'assez bas-niveau, que vous devriez voir en Licence), qu'il vous faudra alors compiler en exécutable machine (un .exe sous Win32).
Je ne connais pas du tout les performances du code produit par mzc, mais vous pouvez aussi vous orienter vers BiglooLinks to a different web site, un excellent compilateur de Scheme vers C, réalisé par Manuel SerranoLinks to a different web site. Bigloo fait partie d'un environnement de développement, BeeLinks to a different web site, que l'on peut tout à fait qualifier de professionnel (il intègre un gestionnaire de projet, un débogger, un profiler, ...).
Notez enfin que, tout autant DrScheme/mzc que Bigloo ajoutent une couche objet au language Scheme : cela peut aussi être l'occasion pour vous de vous frotter un peu plus aux concepts d'objets, de classes, etc...
Si cela vous intéresse, n'hésitez pas à en parler à vos enseignants !



Dernière mise à jour : 02/04/2009 à 23:00
Valid XHTML 1.0!