| Ravi Welcome Documentation Documentation About Ravi Documentation Introduction Premiers pas avec Ravi ravitool Scheme Tutorial Objets Scheme Le shell ravi Starting Ravi Le module trace Les ports d'E/S The C Parser load, require, modules Système d'interruptions Scheme compiler C++ mode Generating C++ Modules La déclaration struct Le type "C-object" More information Installation | Objets SchemeLe Scheme implémenté en Ravi contient une "extension objets" simple et originale; fondamentalement, ce n'est pas bien loin de Smalltalk ou de Java - mais c'est du pur Scheme. Un feature inédit: une classe Scheme peut hériter d'une classe C++. Le langage de définition de la classe parent est alors totalement transparent! Le Scheme-Objet est un vrai langage à objets, avec héritage, classes et métaclasses. Et une vraie extension de Scheme: tous les objets sont définis comme des fonctions, et il n'y a aucun concept de base nouveau. La suite donne
Les principes de base introduisent le langage à objets, et définissent la sémantique en Scheme I Objets et classes
Les champs d'un objet, qui sont déclarés par la définition de classe, sont de plusieurs sortes:
II Extension de Scheme Un objet est une fonction; à savoir: une fonction à 1 argument. L'appel (o ch) a pour valeur le champ de nom ch de l'objet o. {A présent, cela est vrai seulement pour les méthodes, mais ca devrait probablement être élargi à tous les champs!}. Syntaxe de l'appel de méthode est équivalent à un appel de fonction: (-> o m a1 ...) == ((o 'm) a1 ...) Corollaire: une méthode est une fonction; à savoir une fonction qui encapsule son objet. Derrière la syntaxe il y a une différence cruciale: l'écriture (-> o m a1 ...) conduit à une exécution beaucoup plus efficace! (Pas toujours ... cf. paragraphe 4 ci-après).
Ce paragraphe explique la définition d'une classe, la création d'un objet, l'appel de méthode, accès à un champ d'objet. Définition d'une classeLa définition d'une classe se fait par un define-class qui introduit les différents champs d'une classe. (define-classe nom_classe <champ>*) La création d'une instance se fait par la fonction new: (new nom_classe arg1 ...) Les différents champs sont:
Il y a géneralement plusieurs define-method, (à savoir: un par methode), et plusieurs define-class-method; les autres champs ne figurent qu'une fois. Explications define-parents : il y a l'héritage simple seulement (pour l'instant), un seul parent est réellement pris en compte (define-class-variables name (name value) ...) (define-variables name (name value) ...) On définit toutes les variables avec un seul champ, soit avec une valeur initiale, le format est alors (name value), soit sans valeur initiale, le format est alors name. {La valeur par défaut est #f}. L'évaluation se fait comme dans un let: la valeur initiale est une expression quelconque. La valeur d'une variable de classe est évaluée dans le contexte global; la valeur d'une variable d'instance est évaluée dans le contexte de la classe. Constructeur: le constructeur est une méthode qui est appelée automatiquement à la création d'une instance; en cas d'héritage, on appèle d'abord le constructeur de la classe parent (et ceci récursivement). ==> Nombre de paramètres: entorse aux principes Scheme. Les constructeurs des classes supérieures prennent les arguments dont ils ont besoin. S'il y a des arguments en surnombre, il n'y a pas d'erreur. Cela permet de traiter la situation dans laquelle les constructeurs des classes dérivées prennent plus d'arguements que les constructeurs des classes parents. Le format du define-method est exactement celui de la définition de fonctions en Scheme. L'appel de méthode fonctionne uniquement pour les méthodes "normales", c.à d. il ne fonctionne pas pour les méthodes de classe. Création d'un objetLa fonction new marche pour les classes Scheme comme pour les classes C++. (new nom-classe param1 ...) Les objets: mot-clés -> parent-method ::L'appel d'une méthode se fait à l'aide d'un -> Le mot-clé (parent-method) permet d'appeler explicitementune méthode de la classe parent; cela peut être utile si la méthode est "cachée" par une redéfinition dans la classe courante. On écrit (parent-method nom-methode arg1 ...) Le :: permet l'accès aux champs d'une classe, et d'une instance (:: objet champ) pas de quote! Si l'objet est une classe, :: donne le champ défini dans la classe. C'est la seule facon d'accéder aux méthodes de classe et aux variables de classes en dehors des méthodes. Si l'objet est une instance, :: donne le champ de l'instance. Ceci est expérimental, cela revient à enlever toute possibilité à un objet de garder des informations en privé; mais cela semble très pratique. Dans une méthode, la variable this désigne toujours l'objet courant Règles de portéeLa question fondamentale est: où peut-on utiliser les différents champs d'un objet ou d'une classe? Ou, inversement: que peut-on utiliser à tel endroit? La définition d'une classe introduit des environnements imbriquées, à la manière d'un let, ou d'un letrec:
L'héritage crée une imbrication supplémentaire. Cela devient vite compliqué, mais il est toujours possible de traduire des classes en des fonctions Scheme standard - c'est très important pour la portabilité! Les règles suivantes sont une simple conséquence de ce qui précède: La valeur initiale d'une variable de classe est calculée une seule fois, dans l'environnement des classes parentes, à la création de la classe. Les variables de classe sont ensuite connues partout, à l'intérieur de la classe: dans les corps des méthodes, dans les valeurs initiales des variables d'instance. Les méthodes de classe ont la même portée que les variables de classe. Tous les champs d'une classe sont connus dans le corps d'une méthode "normale"; y compris dans le constructeur. Ces règles concernent tous les niveaux d'une hiérarchie; à revoir: l'environnement d'une variable de classe devrait comporter les variables de classe des classes supérieures.
Pour un exemple plus complet, voir ~lux/Ravi/ObjScm/clipsObj.scm où on trouve les définitions des classes pour le réseau Rete de Clips.
;; ============================================================
Reprenons la définition de l'opérateur send comme un appel de fonction: (-> o m a1 ...) == ((o 'm) a1 ...) Dans quelle situation cette écriture est-elle vraiment efficace? Il faut, me semble-t-il, distinguer 2 emplois fort différents. Type A: on imprime des objets stockés dans un liste (for-each (lambda (x) (-> x display)) L) Rien à signaler, l'écriture avec -> s'impose ... très banal, on ne pense pas qu'il peut y avoir une autre configuration! Type B: prenons une table, comme une table d'identificateurs, ou une table d'hypothèses, dans laquelle on veut insérer. On écrit alors (-> tab insert e) Cet appel se trouve dans une espèce de boucle dans laquelle e va varier: on insère différents éléments dans la table. On pourra gagner (un peu) en efficacité en cherchant la méthode insert de l'objet tab 1 seule fois. Pour cela, on transforme la méthode en une fonction globale, en début de programme (define tab-insert (tab 'insert)) qu'on appèle ensuite, dans la boucle de traitement: (tab-insert e) Cette situation est assez fréquente! D'une certaine façon, il s'agit d'une forme d'édition de liens avec un module (qui est un objet).
Même si les objets n'introduisent rien de fondamentalement nouveau, on peut s'attendre à une solution élégante à pas mal de pbs. La classe moduleVieux problème: comment protéger les modules? les conflits de noms? a-t-on besoin de plusieurs oblist?? Un module est un objet de type dictionnaire: il associe des valeurs à des symboles ... et il possède quelques méthodes bien particulières: export-all export unload Le truc réellement nouveau: il n'y a plus de conflit de noms possibles entre différents modules! Du moins, tant qu'on ne les exporte pas; or, l'exportation est toujours facultative, et, en cas de conflit, on peut toujours récuperer une définition là où elle se trouve, dans son module. Si on se refère à la fonction load actuelle: le load crée un module, et effectue systématiquement un export-all; ce dernier point devrait devenir réglable.
On peut étendre une classe C++ avec des définitions Scheme. Si la classe parente est une classe C++ ... toutes les méthodes définies en C++ sont connues comme des définitions internes! {On pourra même utiliser des noms de variables, si elle sont accessibles par des méthodes.} Ca coutera probablement un peu, en place et en temps; mais on gagne en confort! L'objet *father* La variable d'instance *father* désigne l'objet-C++ associé à un objet Scheme. Elle existe pour des raison de compatibilité - on ne devrait pas en avoir besoin, puisque les méthodes de la classe C++ sont héritées. ===========================================
* pas de compilation de define-class * diverses optimisations restent à faire dans l'interpréteur ========================= |