avril
2013
Une particularité intéressante de dodo est qu’il s’agit d’un langage prototypes, à l’instar de Javascript, qui est aussi fortement typé. Comment résoudre cet antagonisme?
Nous y avons déjà touché dans l’article Le Modèle Objet de Dodo. En dodo, l’appartenance à un type implique que la variable dérive d’un prototype donné, et l’interface du type permet d’accéder ou manipuler la variable sur cette base.
Dans cet épisode nous allons nous intéresser particulièrement aux prototypes.
La façon la plus directe d’ajouter des fonctionnalités à une variable est d’étendre son prototype.
Par exemple:
{
double x, y
}
Nous allons décomposer cela.
Cela commence une déclaration de variable. Tous les noms de type commencent avec une majuscule, ce qui n’est pas le cas de point donc il s’agit d’un nom de variable. Le mot def indique l’inférence de type (ou plutôt de prototype) locale, comme « auto » en C++11. On s’attend donc à une valeur d’initialisation.
Comme attendu, voici l’initialisation de la variable introduite par « = ». Le mot-clef new veut dire que l’expression étend un prototype, dans ce cas struct. Toute variable ou constante peut être utilisée comme prototype.
La variable struct est juste une structure vide, elle occupe zéro octet de mémoire et n’a aucun attribut membre. Je vais noter sa valeur {} à la façon de Javascript.
Vu la nature de struct, il n’y a pas vraiment d’utilité à l’utiliser comme prototype si l’on n’ajoute aucun membre. On s’attend donc à ce qu’un bloc de déclaration suive.
double x, y
}
Voici le bloc de déclaration comme prévu. Celui-ci étend le prototype struct et par cela ajoute les attributs x et y en tant que membres de la variable point.
La variable double est utilisée comme prototype de x et y, qui prennent sa valeur (normalement 0.0) puisque ils n’ont pas de valeur d’initialisation. C’est l’équivalent de « def x = double; def y = double ». L’effet et la syntaxe sont les mêmes qu’une déclaration de variable en C++ avec une valeur par défaut.
Quand un prototype est étendu avec de nouveaux attributs membres, l’espace mémoire alloué est la somme de la taille du prototype et la taille des nouveaux attributs. Le prototype est copié au début de l’espace mémoire, suivi par les attributs.
Dans le cas de point la valeur d’initialisation est donc:
{ {}, 0.0, 0.0 }
Ceci occupe l’espace de deux double puisque struct n’occupe aucun espace mémoire.
Cette représentation en mémoire sert de guide à ce que l’on peut faire avec la variable point. Par exemple, on peut lui assigner une nouvelle valeur comme (2.0, -1.33) puisque c’est la notation de dodo pour une structure avec deux valeurs de (proto)type double, donc la représentation en mémoire est la même:
{ {}, 2.0, -1.33 }
Une propriété intéressante d’avoir le prototype en tête de la structure est qu’une variable ainsi structurée est compatible avec son prototype, et le prototype de son prototype, etc… Il suffit de se limiter aux premiers octets de la structure.
Par exemple, si l’on étend le prototype double avec de nouveaux attributs membres pour une variable, cette variable peut être utilisée partout où un double est attendu. Cependant les attributs supplémentaires peuvent être perdus dans l’opération.
Par exemple si l’on fait:
La variable struct_p n’a pas d’attributs x et y car son (proto)type est struct qui ne les a pas définis.
Ce qui est aussi intéressant de voir est quel est le type de retour d’un fonction retournant plusieurs valeurs. En programmation classique, une fonction ne retourne qu’une valeur:
{
return new p(x: p.x + 1.0)
}
Ici la valeur retournée par droite_1(point) est représentée en mémoire par:
{ {}, 1.0, 0.0 }
Mais dodo permet aussi de retourner plus d’une valeur. Cette notation révèle une autre caractéristique du langage dodo, le passage de continuations, qui n’a pas de limitation:
{
return(p.x + 1.0, p.y)
}
La représentation en mémoire de ce que retourne pos_droite_1(point) est:
{ 1.0, 0.0 }
D’après ce que l’on a vu auparavant, le premier élément de la structure est le prototype. Ici c’est un double, donc on peut assigner la valeur de retour à une variable de (proto)type double de par la compatibilité avec le prototype:
Mais dodo est aussi prêt à convertir la valeur de retour en struct étendue où toutes les valeurs retournées sont attributs membres, comme x et y dans point. On peut donc faire:
Où la représentation en mémoire de point1 est la même que pour droite_1(point):
{ {}, 1.0, 0.0 }
Il y a aussi des variables dont la représentation en mémoire dépend de l’état d’un ou plusieurs attributs membres. C’est le cas en particulier de class, le prototype des objets, qui a pour seul attribut une lien vers l’interface de l’objet:
{ Object }
L’interface de class est Object.
Toute classe d’objets qui étend ce prototype a un lien vers sa propre interface, ce qui permet à dodo de connaître sa représentation en mémoire.
Par exemple:
{
point centre
}
L’instance par défaut de cette classe, l’objet Figure.instance, est représenté en mémoire par:
{ Figure, { {}, 0.0, 0.0 } }
On peut définir la classe d’objets Cercle qui étend Figure:
{
double rayon = 1.0
}
Comme auparavant, le prototype est le premier membre de la structure pour Cercle.instance:
{ { Cercle, { {}, 0.0, 0.0 } }, 1.0 }
Mais si on assigne ceci à une variable de (proto)type Figure.instance, la valeur du lien (attribut dont dépend la représentation en mémoire) doit être changé en Figure:
Si ce n’était pas le cas la variable figure mentirait sur son état, ce qui cause des problèmes. Dodo utilise la relation entre Cercle et Figure pour déterminer la valeur à utiliser pour le lien. En fin de compte la variable est représentée par:
{ Figure, { {}, 0.0, 0.0 } }
Cela illustre bien que les prototypes ne se prêtent pas bien au polymorphisme. Si l’on veut préserver la nature de Cercle de l’objet, il faut utiliser le nom de la classe plutôt que son instance comme type:
Cette fois-ci, la représentation en mémoire de l’objet figure_cercle reste inchangée car il n’y a pas de contrainte sur la façon dont il est représenté:
{ { Cercle, { {}, 0.0, 0.0 } }, 1.0 }