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

[PREV][SUIV]

Objets Scheme

Le 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

Les principes de base introduisent le langage à objets, et définissent la sémantique en Scheme

I Objets et classes

  • Tout objet est instance d'une classe.
  • Une classe est un objet.
  • Les classes forment une hiérarchie d'héritage.

Les champs d'un objet, qui sont déclarés par la définition de classe, sont de plusieurs sortes:

  • variable d'instance
  • méthode
  • variable de classe
  • fonction de classe

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).

Syntaxe et mode d'emploi

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 classe

La 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:


 (define-parents name)
 (define-class-variables name (name value) ...)
 (define-class-method (name . params) body)
 (define-variables name (name value) ...)
 (define-method (name . params) body)
 (define-constructor (lambda args body)

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 objet

La 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.

this

Dans une méthode, la variable this désigne toujours l'objet courant

Règles de portée

La 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:

  • les variables de classe
  • les méthodes de classe
  • les variables d'instance
  • les méthodes (y compris le constructeur)

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.

Un exemple

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.


(define-class cl1

(define-class-variables (cv1 'a) (cv2 1)) (define-variables v1 (v2 cv2)) (define-class-method (cm1 x) (cons cv1 x)) (define-constructor (lambda (x) (display-l "constructeur cl1 " x #\newline) (set! v1 x))) )

(define-class cl2

(define-parents cl1) (define-class-variables (cz1 'b) (cz2 ())) (define-variables (z1 'valz1) z2) (define-class-method (mcl2 . l) (mcons cz1 cv1 l)) (define-constructor (lambda (x) (display-l "constructeur cl2 " x #\newline) (set! z2 x)))

(define-method (mm2) (display-l "dans mm2 " mm3 #\newline) (set! z1 'toto) (list cm1 mm3))

(define-method (mm3 a) (display-l "dans mm3, a = " a ", z1 = " z1 #\newline) (mm2))

(define-method (mm4 x) (set! v1 x) v1 ) (define-method (mm5) cz1) (define-method (mm6) z1) (define-method (mm7 x y) (set! cv1 y) (display-l "mon x " x " et y " y #\newline "cv1 " cv1 " cz1 " cz1 #\newline "v2 " v2 " z1 " z1 #\newline "z2 " z2 #\newline "cm1 " cm1 " mm2 " mm2 #\newline))

)

;; ============================================================

Efficacité du send

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).

Discussion: méta-objets et modules

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 module

Vieux 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.

Les classes C++

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.

===========================================

Limitations provisoires

* pas de compilation de define-class

* diverses optimisations restent à faire dans l'interpréteur

=========================