En informatique, les «variables» associent un nom (c-à-d, un symbole, un identifiant) à une valeur ou un objet dans la mémoire de l'ordinateur. C'est le plus souvent ce concept généralisé qui est retenu par la plupart des programmeurs. Bien sûr, chaque langage de programmation implémente ce concept en fonction de sa propre philosophie et des paradigmes qu'il met en application. Sous ce rapport, Abstrasy propose un système de variables très puissant1).
Nous développons quelques notions importantes ci-dessous.
Le contenu d'une variable, c'est tout simplement la donnée que l'on assigne à un symbole (le nom de la variable).
Il est important de noter qu'Abstrasy lie directement un symbole à un contenu et que ce lien est immuable. Il n'y a donc aucun risque de rencontrer les problèmes ou les effets de bords que l'on peut observer dans les langages de programmation qui autorisent l'affectation destructive (où on modifie le lien entre la donnée et le symbole pour que celui-ci pointe vers la nouvelle donnée qui l'affecte).
En réalité, l'affectation destructive n'existe pas en Abstrasy. Toute donnée liée à un symbole reste attachée à celui-ci indéfiniment. Ce principe est conforme à la programmation fonctionnelle qui permet entre autre de supprimer de nombreuses causes d'effets de bord indésirables et d'améliorer ainsi la qualité des programmes.
À la lecture de ce qui précède, vous vous demandez peut-être comment on peut encore parler de «variables» si le contenu d'une «variable» est immuable. Ne devrait-on pas parler de «constantes» dans ce cas?…
Non, pas nécessairement. Voyons pourquoi.
Si le lien qui unit le symbole d'une variable à son contenu est immuable, rien n'interdit de lier le symbole à un contenu qui est lui-même un contenant. Le lien qui unit le symbole au contenant peut alors être immuable, cela n'exclu pas l'affectation du contenu lié au contenant. Le contenant est alors un dispositif d'indirection qui permet d'atteindre la ou les données.
C'est de cette manière que l'on utilise les conteneurs abstraits (listes, piles, etc…) dans la plupart des langages de programmation. Ces conteneurs abstraits ajoutent une couche d'abstraction qui implémente l'organisation et l'accès aux données. Généralement, on ne les utilisent que pour les collections. Mais Abstrasy étend et impose même ce concept à toutes les variables mutables. Ainsi, bien qu'on ne puisse pas altérer la liaison qu'il y a entre le symbole et le contenant, on peut affecter le contenu du contenant en fonction de l'implémentation de ce dernier. Bien sûr, cette implémentation est conçue pour être utilisée d'une manière transparente.
Abstrasy applique le principe du typage dynamique fort du contenu ainsi qu'un typage statique du contenant.
Le typage dynamique consiste à fixer le type des données que l'on peut associer à une variable à la volé durant le «runtime». Il s'agit donc de la stratégie de typage la plus appropriée pour un langage de programmation dynamique interprété comme Abstrasy.
Rappelons que le typage dynamique s'oppose au typage statique qui est fixé au moment de la compilation des programmes et non durant l'exécution.
Le choix de la stratégie de typage est souvent à l'origine de débats interminables, voir même de polémiques. En réalité, chaque stratégie apporte des avantages, mais aussi des inconvénients. Ainsi, pour le langage Abstrasy, un langage dynamique interprété, le typage dynamique du contenu semble la stratégie la plus appropriée. La stratégie de typage statique appliquée durant le «runtime» nécessiterait d'adopter un style de programmation défensive qui serait très peu performant.
La stratégie de typage dynamique adoptée est cependant forte. Cela signifie que dès lors qu'une donnée typée est assignée à un symbole, on ne peut plus lui affecter une donnée d'un autre type. C'est aussi une particularité que l'on retrouve dans d'autres langages dynamiques, comme Python par exemple. Il n'est donc pas possible d'affecter une chaîne de caractères à une variable mutable qui contient déjà un nombre. Ainsi, pour typer le contenu d'une variable, il suffit de l'initialiser avec une valeur du type souhaité. Par exemple, (define '$x 0) fixe le type du contenu de la variable $x de telle sorte qu'elle ne puisse contenir que des nombres entiers.
Abstrasy a aussi la particularité d'appliquer un typage statique du contenant en imposant l'utilisation de règles de nommage des symboles. De cette manière, le symbole fourni une documentation relative au contenant (Pour d'avantage de renseignements à ce sujet, veuillez consulter la partie «Symboles»).
Les symboles des variables immuables commencent par une lettre. Celle-ci peut être en majuscule ou en minuscule. Toutefois, la case des caractères est prise en compte pour distinguer les symboles. Ainsi, par exemple, le symbole a représente une autre variable que celle liée au symbole A, mais les deux font partie des variables immuable. Il en va de même de toutes les variables dont le symbole commence par une lettre comme par exemple x1, pi, curInterface25, etc…
Une variable immuable ne peut plus voir son contenu affecté après l'initialisation et l'assignation à un symbole. Les variables immuables sont donc préservées contre toute affectation accidentelle.
Les symboles des variables mutables commence toujours par un caractère dollar $. Ce préfixe indique explicitement que le contenu de la variable peut muter.
En réalité, l'interpréteur assigne un objet ref (référence) au symbole. Le symbole est donc assigné à un contenant qui fourni la couche d'abstraction nécessaire pour supporter l'affectation d'un contenu. Bien sûr, la liaison entre le symbole et le contenant est immuable, mais le contenu du contenant peut muter.
Par exemple, les variables $a, $surface48 ou encore $mix-4-U sont des variables mutables.
Les objets ref sont instanciés automatiquement lors de la création de la variable et l'affectation du contenu respecte le principe de typage dynamique fort.
Abstrasy implémente les thunks. Il s'agit d'un contenant qui capturent un contexte et une expression dont l'évaluation est différée. Ce n'est que lorsque la valeur d'un thunk est absolument nécessaire que son évaluation est finalisée pour en connaître le résultat. Dans d'autres langages de programmation, on parle parfois de «suspended computation» ou de «delayed computation». Étant donné qu'un thunk capture statiquement le contexte dans lequel il a été créé, on peut aussi le considérer comme une fermeture non paramétrée (en anglais, «parameterless closure»). D'un point de vue fonctionnel, un thunk s'apparente à une lambda expression dépourvue de paramètre qui retourne un résultat immuable et mémoïsé. L'évaluation d'un thunk est réalisée uniquement selon les besoins («call by need»).
Comme une variable ne peut être assignée qu'à une valeur, on ne peut pas assigner un thunk à une variable classique. Une telle assignation rendrait nécessaire l'évaluation du thunk. De plus, comme la valeur d'un thunk est immuable, on ne peut pas l'assigner non plus à une variable mutable. Aussi, Abstrasy propose des variables à évaluation paresseuse. Ces variables disposent d'un symbole statiquement typé dont le nom commence par un tilde ~. On peut ainsi facilement distinguer les variables à évaluation paresseuse des autres types de variables disponibles.
(define '~a (thunk{+ 10 5})) (display ~a)
⇒
15
Dans l'exemple ci-dessus, on assigne le thunk (thunk{+ 10 5}) à la variable dont le symbole est ~a.
A première vue, il n'y a rien de spécial. Mais le thunk n'est pas évalué au moment de l'assignation de la variable, mais bien lorsqu'on ne peut plus l'utiliser sans connaître son résultat, c-à-d dans l'expression (display ~a).
Pour nous en assurer, modifions légèrement notre script d'exemple et observons le résultat.
(display "A") (define '~a (thunk{ (display "B") (+ 10 5) })) (display "C") (display ~a) (display "D")
⇒
A C B 15 D
Comme on peut le voir, le contenu du thunk est bien évalué lorsque son résultat est demandé par (display ~a).
L'utilisation d'une variable dont le nom commence par un tilde ~ est important car cela documente le comportement de la variable. Cela protègle le thunk de la valorisation au moment de l'assignation. Si on avait écrit (define 'a (thunk{+ 10 5})), le thunk aurait été évalué immédiatement car seule une valeur peut être assignée à une variable immuable. De même, si nous avions écrit (define '$a (thunk{+ 10 5})), le thunk aurait également été évalué immédiatement parce que le contenant ref d'une variable mutable ne peut contenir que des valeurs. Dans (ref (thunk{10})), la cellule d'indirection fait référence au résultat 10 est non au thunk lui-même.
Les variables à évaluation paresseuse s'utilise donc comme des variables dont le contenu est immuable. Toutefois, elles présentent l'avantage de «geler» l'évaluation du contenu à moins que celui-ci soit absolument nécessaire. De cette manière, il est possible de déclarer à l'avance des variables immuables qui sont le fruit d'un certain calcul plus ou moins important sans nécessairement réaliser ce calcul immédiatement. La valeur de ces variables sera calculer uniquement si c'est nécessaire. Il s'agit donc d'une optimisation.
La portée des variables est délimitée statiquement par l'étendue de l'expression paresseuse (c-à-d, une expression délimitée par des accolades { et }) dans laquelle la variable est déclarée. La portée d'une variable s'étend donc également à toutes les expressions imbriquées dans celle qui en détermine les limites.
En outre, il n'existe pas de variable à portée globale. Toutes les variables ont une portée locale uniquement. Ainsi, même si des variables sont définies dans le contexte initial d'un programme et que, par voie de conséquence, leur portée s'étend à tout le programme, elles restent des variables locales du programme.
(define 'x 0) (display x) (if{zero? x} { (define 'x 1) (display x) } ) (display x)
⇒
0 1 0
Dans l'exemple ci-dessus, on définit x et on lui assigne la valeur 0 dans le contexte du programme. Cette valeur est accessible dans le contexte de l'expression du prédicat {zero? x}, mais celle-ci est masquée dans le contexte de l'expression qui forme le corps de la structure conditionnelle. Là, x vaut 1. Ensuite, lorsqu'on sort de l'expression imbriquée de la structure conditionnelle, on retrouve la valeur de x initiale, c-à-d 0.
Il est donc possible de redéfinir une variable dans un sous-contexte. La nouvelle variable masque alors la précédente. Toutefois, il est interdit de définir plusieurs fois la même variable dans le même contexte.
(define '$x 10) (define '$x 50)
⇒
ERROR... @000002> Trace: (define '$x 50) @000002> Symbol already defined ($x)
On dit également que la portée d'une variable est statique car elle est définie par la position de sa déclaration dans le code. Illustrons cela par un exemple.
(function 'Compteur { (args '$i) (return (function { (define 'r $i) (set! $i (+ $i 1)) (return r) }) ) }) (define 'x (Compteur 10)) (define '$i 0) (display (x)) (display (x)) (display (x))
⇒
10 11 12
Ici, nous créons une fonction d'ordre supérieur (donc qui retourne une fonction). Cependant, le paramètre que reçoit cette fonction est utilisé comme accumulateur. Il s'agit de la variable locale $i située dans le corps de la fonction d'ordre supérieur Compteur.
Pour corser le problème et nous assurer que la résolution de portée est bien statique, nous déclarons une variable du même nom $i dans le contexte du programme, puis nous évaluons la fonction retournée x.
Nous pouvons constater que le résultat démontre que la variable utilisée est bien $i dans le corps de la fonction d'ordre supérieur et non la variable $i déclarée dans le corps du programme. La résolution est donc bien statique.
Cet exemple montre également que le langage Abstrasy supporte les fermetures (ou «closures» en anglais). Et en programmation par objet, cette caractéristique permet, entre autre, de mettre en oeuvre les variables privées d'un objet.