Abstrasy autorise un haut niveau d'abstraction grâce à la généricité.
Rappelons que pour être générique, une fonction doit pouvoir offrir une certaine indépendance par rapport au type de ses arguments et fournir un résultat cohérent sans nécessiter une définition particulière pour chaque cas où le type des arguments est différent.
Pour cela, Abstrasy propose une expressivité particulièrement uniforme qui est donc fondamentalement générique. Mais ce n'est pas tout. Le langage supporte également le transtypage. Cette opération consiste à convertir le type d'une donnée dans un autre pour le rendre utilisable par une fonction ou un opérateur.
Bien entendu, on peut penser à la coercition très souvent utilisée dans des langages comme Java ou Python. La coercition permet notamment d'utiliser les objets d'une classe comme des instances d'une de ses super-classes. Cette opération est souvent appelée casting. Il est toutefois important de noter que le cast n'est pas une conversion, mais bien d'une vérification du type de la donnée. En effet, on s'assure simplement que la donnée hérite bien des caractéristiques d'un type donné avant de l'enregistrer et de l'utiliser en tant que donnée de ce type. Cette opération préliminaire est indispensable dans le cas des langages de programmation statique et cela même dans lorsqu'on implémente la généricité. Ainsi, par exemple, même si le conteneur Java ArrayList<T> est générique, celui-ci doit être typé au moment de l'instanciation (par exemple en ArrayList<Object>). Si ce n'est pas la cas, le compilateur lèvera un avertissement.
Abstrasy réalise aussi la coercition. Mais comme il s'agit d'un langage de programmation dynamique, celle-ci est implicite et le programmeur n'a pas vraiment à s'en soucier. Bien sûr, en cas de nécessité, on peut vérifier l'hérédité d'un objet à l'aide du prédicat inherits? (qui est dans ce cas l'équivalent en Abstrasy de l'opérateur instanceof Java). Pour le reste, Abstrasy adopte une stratégie générique en limitant les contraintes de types au strict minimum. Cette abstraction est mise en œuvre par l'intermédiaire des traits génériques que possèdent les données.
La conversion est une opération plus profonde que la coercition. Lorsqu'une donnée est convertie, une nouvelle donnée du type cible est créée. Celle-ci est paramétrée pour que sa représentation dans le type cible corresponde à celle de la donnée originelle dans le type de départ. Le but est généralement de substituer la donnée de départ par celle qui lui correspond dans le type cible. On peut ainsi réaliser certaines opérations implémentées dans le type cible.
Lorsqu'une conversion est explicite, cela signifie que la conversion est apparente dans le code du programme. Illustrons cela par un exemple simple:
(display (+ 5.0 (number "10.0")))
⇒
15.0
Ici, nous avons utilisé l'opérateur number pour convertir explicitement la chaîne de caractères “10.0” en nombre 10.0. Ainsi, l'opération (+ 5.0 10.0) a pu être réalisée et son résultat est bien 15.0. Donc, en l'occurrence, l'opérateur number a permis de réaliser la conversion explicite de la chaîne de caractères en nombre.
Sans cette conversion explicite, l'interpréteur aurait signalé une erreur puisqu'on ne peut pas ajouter directement une chaîne de caractères à un nombre.
(display (+ 5.0 "10.0"))
⇒
ERROR... @000001> Trace: (display (+ 5.0 "10.0")) @000001> Type mismatch
La conversion peut aussi être implicite. Dans ce cas, on peut faire abstraction de la conversion, éventuellement nécessaire, des données avant de réaliser une opération.
Ainsi, Abstrasy autorise la conversion implicite des types de nombre. Ceci permet, par exemple, d'ajouter un nombre entier à un nombre à virgule flottante sans le convertir explicitement.
(display (+ 5.5 50))
⇒
55.5
Cela ne signifie pas qu'il n'y a pas de conversion. Dans l'exemple ci-dessus, le nombre entier 50 est implicitement converti en flottant 50.0 avant d'être ajouté à 5.5, ce qui donne bien 55.5.
Il est toutefois important de noter que la conversion implicite ne peut être réalisée que dans des conditions sûres. En effet, dans l'exemple ci-dessus, nous avons bénéficié de la conversion implicite parce que tous nombre du type entier existe aussi sous la forme d'un nombre de type flottant. Cependant, l'inverse n'est pas vrai.
(display (+ 5 50.0))
⇒
ERROR... @000001> Trace: (display (+ 5 50.0)) @000001> Type coercion error
Si on essaye d'ajouter un nombre flottant à un nombre entier, on obtient une erreur. Même si le nombre flottant existe aussi en temps que nombre entier, il n'en demeure pas moins que tous les nombres flottants n'existe pas en tant que nombre entier. Ainsi, le flottant 3.2 n'existe pas en tant que nombre entier. Il peut éventuellement être arrondi, mais même dans ce cas, la conversion implicite peut devenir une source d'erreur en raison de son ambiguïté. Ainsi, dans ce cas, on est obligé d'utiliser une conversion explicite même s'il s'agit d'un nombre.
(display (+ 5 (int 50.0)))
⇒
55
L'implémentation de la conversion implicite est réalisée au niveau de la fonction ou de l'opérateur et non au niveau du type lui-même.
Illustrons cela par un exemple simple. Imaginons que nous créons une fonction qui additionne deux nombres et retourne le résultat. Nous aurons donc:
(function 'add-as-number { (args 'a 'b) (return (+ a b)) } )
Il n'y a effectivement pas plus simple. Testons à présent cette fonction et voyons comment elle se comporte.
Si on appelle la fonction avec deux entiers, on obtient:
(add-as-number 5 10)
⇒
15
Si on l'appelle à présent avec un flottant et un entier, le résultat est bien évidemment celui-ci:
(add-as-number 5.0 10)
⇒
15.0
Le résultat est un flottant puisque notre fonction hérite du comportement de l'opérateur +. Ainsi, comme on peut s'y attendre, notre fonction ne réalise pas de conversion implicite pour autre chose qu'un nombre entier, flottant, long entier ou long flottant.
(add-as-number 5.0 "10")
⇒
ERROR... @000008> Trace: (add-as-number 5.0 "10") ... @000004> Trace: (return (+ a b)) @000004> Type mismatch
Pour implémenter la conversion implicite du deuxième argument de la fonction, il suffit d'adapter notre fonction en conséquence. Très simplement on a:
(function 'add-as-number { (args 'a 'b) (return (+ a (number b))) } )
Ou on réalise une conversion explicite du deuxième argument dans le corps de la fonction. Le comportement de notre fonction admet donc à présent la conversion implicite en nombre du deuxième argument quelque soit son type. Ainsi, par exemple, nous obtenons maintenant:
(add-as-number 5.0 "10")
⇒
15.0
Si on souhaite élargir la conversion implicite au concept de nombre générique number, il suffit d'ajouter la conversion explicite du premier argument dans le corps de la fonction pour implémenter la conversion implicite de cet argument. Conversion implicite que l'on entend au moment de sont utilisation.
(function 'add-as-number { (args 'a 'b) (return (+ (number a) (number b))) } )
On peut donc à présent l'utiliser comme ceci:
(add-as-number "5.0" "10")
⇒
15.0
Remarquez que la conversion du premier argument à l'aide de number a permis d'obtenir un flottant puisque sa forme en tant que chaîne de caractère est “5.0”. number permet de convertir une donnée dans un nombre d'un point de vue générique. Il est important de retenir également que tous les types ne peuvent pas être converti en un nombre (même d'un point de vue élargi). Si on essaye par exemple de convertir un tuple en nombre, on obtient une erreur. Pour permettre sa conversion en un nombre, ou en tout autre type spécifique ou générique, un objet doit implémenter le trait qui correspond à l'opérateur de conversion. De cette manière, on implémente les conversions explicites que l'on peut appliquer à l'objet. Toutefois, les conversions implicites doivent être implémentées directement dans le corps des fonctions ou des méthodes qui utilisent cet objet.
|
![]() |
|
||||||||