Rappelons que le type d'une donnée définit les valeurs qu'elle peut prendre ainsi que les opérateurs qui peuvent lui être appliqués.
Il est important de retenir également que tous les éléments du langage Abstrasy ont un type. Cela signifie aussi que les éléments du langage peuvent être manipulés comme des données, y compris les opérateurs et les fonctions.
Abstrasy propose plusieurs types prédéfinis. Ceux-ci sont fournis avec le paquetage de base du langage. Il n'est donc pas nécessaire d'ajouter de bibliothèques externes supplémentaires pour utiliser les types prédéfinis.
Plusieurs types prédéfinis peuvent être utilisés directement sans nécessité d'opération préliminaire comme, par exemple, l'importation d'un module particulier. Ils sont directement disponible avec le langage de base. On dit alors qu'ils sont primitifs.
A l'exception des types object et adapter, les types primitifs ne sont pas dérivables.
La plupart des types primitifs s'expriment de façon littérale. En effet, lorsque la valeur d'une donnée primitive est immuable, cette valeur peut être exprimée directement dans le code source du programme. Les littéraux sont des constructeurs.
Voici quelques exemples:
# La forme littérale d'une chaîne de caractères est délimitée par des guillemets. (define 'chaine "Chaîne de caractères") # Les formes littérales des nombres imposent également la distinction explicite du type: # - un entier (32 bits). (define 'entier 10) # - un entier long (64 bits) (define 'entier-long 123987654321L) # - un nombre à virgule flottante IEEE754 - 64 bits. (define 'flottant 5.67) # - un nombre à virgule flottante IEEE754r - Decimal128. (define 'flottant-long 5.67e-983L) # La forme littérale d'un tuple est également explicite: (define 'un-tuple [1 "pomme"])
Comme vous pouvez le constater dans les exemples ci-dessus, les formes littérales sont explicites et fournissent des marqueurs qui permettent de reconnaître facilement le type respectif de chaque donnée.
Par exemple, un nombre à virgule flottante se distingue d'un nombre entier par la présence du point décimal. Ainsi, 10 est le nombre entier dont la valeur est dix, par contre, 10.0 est le nombre à virgule flottante dont la valeur est dix. Même si leur valeur est équivalente, ils ne sont pas identiques (test d'identité).
(display (=? 10.0 10)) (display (==? 10.0 10))
⇒
(true) (false)
Notez toutefois que le test d'identité en Abstrasy ne tient compte que de l'emplacement de la donnée dans la mémoire. Ainsi, si on teste (==? 10 10), on obtient toujours (false). On obtient ce résultat parce que tout est objet en Abstrasy. Ainsi, l'instance du premier nombre 10 de l'expression (==? 10 10) ne se trouve pas au même emplacement mémoire que le second. Lorsque l'expression est évaluée, le premier littéral 10 est évalué et l'objet 10 est créé. Lorsque le second est évalué, une nouvelle instance 10 est créée. De fait, l'identité de la première instance du nombre 10 est différente de la deuxième. C'est un comportement que l'on retrouve également dans d'autres langages de programmation comme Groovy par exemple. Abstrasy fait donc une distinction nette entre l’égalité de valeur et l'identité.
(define 'a 10) (define 'b a) (display (==? a b)) (display (==? a 10))
⇒
(true) (false)
Dans l'exemple ci-dessus, les variables immuables a et b ont bien la même identité puisqu'elles symbolisent la même instance unique 10. Par contre, a n'a pas la même identité que le littéral 10 proposé dans le deuxième test. Bien que leur valeur soit la même, il s'agit effectivement de deux instances distinctes.
Il existe aussi des types primitifs qui nécessitent l'utilisation d'un constructeur. Dans ce cas, le type de donnée est généralement une structure composite immuable et le constructeur est un opérateur.
Par exemple:
(define 't (array [1 2 3]))
Dans cet exemple, nous assignons au symbole t une nouvelle instance du type array. Un array est un tableau immuable composé de n valeurs, elles aussi immuables. Ici, le constructeur array crée le nouveau tableau à partir d'un tuple, il s'agit donc d'une conversion.
Mais, certains constructeurs ne se limite pas à réaliser une conversion. Par exemple, il est possible de créer un vecteur immuable à l'aide de l'opérateur vector.
(define 'v (vector 3)) (display v)
⇒
(vector[(nothing) (nothing) (nothing)])
Ici, nous avons simplement indiquer la taille du vecteur immuable. Rappelons à ce propos que c'est la structure du type vector qui est immuable, et non son contenu. Nous pouvons donc affecter le contenu du vecteur à l'aide de set!.
(define 'v (vector 3)) (set! (v 0) 10) (set! (v 1) 20) (set! (v 2) 30) (display v)
⇒
(vector[10 20 30])
Notez également que certains constructeurs primitifs fournissent toujours la même instance singleton. C'est le cas notamment de (true), (false) et (nothing). Les instances retournées par ces constructeurs répondent donc parfaitement au test identitaire.
(display (==? (false) (false))
⇒
(true)
Mis à part les objets des types object et adapter, aucun type primitif ne peut être hérité. On ne peut pas , par exemple, créer un objet qui hérite directement des propriétés d'un nombre entier, à moins de créer préalablement un objet «enveloppe» (en anglais, «wrapper») de manière à fournir un «adaptateur» dérivable pour l'objet «nombre entier» qui est lui non dérivable.
Rappelons que tous les objets ont une existence concrète dans la mémoire de l'ordinateur (qui correspond également à leur identité). Lorsqu'il s'agit d'objets du type object ou adapter, ceux-ci offrent une interface qui peut être héritée par un autre objet du type object. Ces objets sont appelés des prototypes.
La méthode la plus simple pour construire un prototype consiste à utiliser l'opérateur object.
object est un constructeur d'objet générique. Il peut être utilisé d'une manière similaire à celle d'un constructeur dynamique en Javascript
(object 'rectangle { (define ':largeur 3) (define ':hauteur 4) (function ':surface {return (* :largeur :hauteur)}) }) (display (rectangle:surface))
⇒
12
Abstrasy autorise en effet la construction dynamique des prototypes. Pour cela, il suffit de créer un prototype générique, puis de l'étendre dynamiquement en lui assignant les méthodes et propriétés souhaitées. L'exemple ci-dessus peut donc également s'exprimer comme ceci, tout en offrant le même résultat.
(object 'rectangle { }) (define 'rectangle:largeur 3) (define 'rectangle:hauteur 4) (function 'rectangle:surface {return (* :largeur :hauteur)}) (display (rectangle:surface))
⇒
12
Tout objet du type object peut hériter des propriétés d'un autre objet du type object.
(object 'rectangle { (define '$largeur 0) (define '$hauteur 0) (function ':set-largeur{ (args 'largeur) (set! $largeur largeur) }) (function ':set-hauteur{ (args 'hauteur) (set! $hauteur hauteur) }) (function ':surface {return (* $largeur $hauteur)}) }) (object 'carre { (extends rectangle) (function ':set-cote{ (args 'cote) (..:set-largeur cote) (..:set-hauteur cote) }) (define ':set-largeur :set-cote) (define ':set-hauteur :set-cote) }) (carre:set-cote 5) (display (carre:surface)) => <code> 25
Bien entendu, on peut aussi rendre plus abstraite la construction des prototypes en utilisant des fonctions «constructeurs». Dans ce cas, on peut même utiliser la portée des fermetures respectives des fonctions de construction pour, par exemple, y stocker des variables privées passées comme des arguments.
(function 'Rectangle { (args '$largeur '$hauteur) (return (object { (function ':set-largeur{ (args 'largeur) (set! $largeur largeur) }) (function ':set-hauteur{ (args 'hauteur) (set! $hauteur hauteur) }) (function ':surface {return (* $largeur $hauteur)}) }) ) }) (function 'Carre { (args '$cote) (return (object { (extends (Rectangle $cote $cote)) (function ':set-cote{ (args 'cote) (..:set-largeur cote) (..:set-hauteur cote) }) (define ':set-largeur :set-cote) (define ':set-hauteur :set-cote) }) ) }) (define 'c (Carre 5)) (display (c:surface)) (c:set-largeur 6) (display (c:surface))
⇒
25 36
Un autre type très intéressant est le type constructor ou «constructeur».
Le dernier exemple ci-dessus met en œuvre des fonctions «constructeurs». Il s'agit d'une méthode que l'on retrouve également dans d'autres langage de programmation orienté prototype, comme JavaScript par exemple. Cependant, Abstrasy propose une méthode plus élégante pour créer des nouvelles instances d'objets basés sur un modèle abstrait.
(constructor 'Rectangle { (args '$largeur '$hauteur) (function ':set-largeur{ (args 'largeur) (set! $largeur largeur) }) (function ':set-hauteur{ (args 'hauteur) (set! $hauteur hauteur) }) (function ':surface {return (* $largeur $hauteur)}) }) (define 'r (Rectangle 5 10)) (display (r:surface))
⇒
50
Les constructor sont des fonctions de construction. Un constructor peut, comme une fonction, recevoir des arguments qui peuvent être utilisé pour initialiser l'objet qui sera créé. Bien sûr, lorsqu'on appelle un constructor, on crée une nouvelle instance d'un objet dont le constructeur est le modèle. C'est pourquoi, les instances héritent de leur constructeur.
(constructor 'Rectangle { (args '$largeur '$hauteur) (function ':set-largeur{ (args 'largeur) (set! $largeur largeur) }) (function ':set-hauteur{ (args 'hauteur) (set! $hauteur hauteur) }) (function ':surface {return (* $largeur $hauteur)}) }) (define 'r (Rectangle 5 10)) (display (inherits? r Rectangle))
⇒
(true)
Notez qu'un objet ne peut pas hériter d'une fonction, c'est pourquoi, il est généralement préférable d'utiliser un constructor. Lorsqu'on crée un objet à l'aide une fonction, cet objet est généralement un objet générique que l'on étend dynamiquement. Son type est donc anonyme. constructor permet de définir le modèle d'un type nommé. Aussi, l'utilisation d'un constructor pour créer des instances permet de fournir à ces instances un type nommé également.
Cela dit, les constructor ne sont pas des classes. Les constructor fournissent les modèle de construction concret des instances d'objets des types nommés auxquels ils correspondent respectivement. Contrairement aux classes, les constructeurs ne peuvent hériter d'autres constructeurs. Seuls les prototypes bénéficient de l'héritage. En programmation orientée prototype, l'héritage est obtenu par délégation entre objets concrets. Aussi, pour mettre en œuvre l'héritage, il suffit de faire apparaître cette délégation dans le corps des constructeurs.
(constructor 'Rectangle { (args '$largeur '$hauteur) (function ':set-largeur{ (args 'largeur) (set! $largeur largeur) }) (function ':set-hauteur{ (args 'hauteur) (set! $hauteur hauteur) }) (function ':surface {return (* $largeur $hauteur)}) }) (constructor 'Carre { (args '$cote) (extends (Rectangle $cote $cote)) (function ':set-cote{ (args 'cote) (..:set-largeur cote) (..:set-hauteur cote) }) (define ':set-largeur :set-cote) (define ':set-hauteur :set-cote) }) (define 'o (Carre 5)) (display (o:surface)) (display (inherits? o Carre)) (display (inherits? o Rectangle))
⇒
25 (true) (true)
Dans cet exemple, les instances d'objet du type Carre étendent une instance concrète du type Rectangle. Les appels des instances Carre sont interceptés et redirigés aux méthodes appropriées. Dans le cas de la méthode :surface, celle-ci n'est pas définie dans les instances Carre. Elle est donc déléguée (ou transférée) à l'instance Rectangle dont l'instance Carre hérite.
|
![]() |
|
||||||||