View this PageEdit this PageAttachments to this PageHistory of this PageHomeRecent ChangesSearch the SwikiHelp Guide

types

!!!!!Article en cours de rédaction!!!!!

Définition

Un type est la "description d'un ensemble d'objets muni d'un certain nombre d'opérations. Dans l'approche orientée objet, chaque type est fondé sur une classe" (B. Meyer, Conception et programmation orientées objet p.1165).

Cette définition est discutable mais est un bon départ.

Il est important de bien typer ses objets, c'est un gage de découverte rapide de ses erreurs, ainsi, utiliser abusivement la représentation par des entiers (le cas le plus fréquent) ne conduira qu'a diminuer la visibilité de ses erreurs. Par exemple, représenter les tokens d'un analyseur lexical par un entier va rendre invisible une opération aussi dénuée de sens qu'une addition de token, diminuant la qualité globale du code.

D'autre part, le programme doît être le plus proche possible de la réalité et tenter de faire l'ellipse de la représentation en mémoire des objets. L'idéal serait de séparer la partie utilisation des objets de la description de leur représentation en mémoire.

Un peu de taxinomie

Dans les divers langages de programmation existants dans la nature, on peut dégager 2 critères orthogonaux :

Un typage faiblement contraint ("typage faible") offre la possibilité de changer le type d'un objet dynamiquement sans garantie sur le résultat (le cast du C par ex.) et ainsi la possibilité de faire échouer un type (on a tous eu un jour ou l'autre un problème de signed int/unsigned int dans un printf() en C). A l'opposé, le typage fort rend impossible tout changement de type qui n'est pas garanti contre les erreur (par ex. en VHDL on ne peut faire passer un UNSIGNED(7 downto 0) sur un bus BIT_VECTOR(7 downto 0) sans une fonction aritmétique malgré l'apparence trivile de la conversion). Le typage fort est le standard dans les langages modernes bien qu'il est très ancien (LISP en 1958 possédait ce typage). Sauf exceptions le typage faible sera ignoré par la suite.

Un système de types statique offre la possibilité de savoir a priori le type d'un objet référencé (ou contenu) par une variable ou un attribut (par ex. let a:int = 3 en Objective Caml), les fonctions possèdent aussi des information de type dans leur signature (par ex. int atoi(const char ) en C). On dit aussi que les variables sont typées bien que se soit réducteur (puisque les fonctions le sont aussi). Dans l'autre sens, les langages dynamiquement typés, offrent la possibilité de vérifier le type d'un objet à l'exécution, ils conservent ainsi des information de type dans la mémoire (par ex. Transcript nextPutAll: self class printString en Smalltalk tire partie du typage dynamique). Il est important de noter que les aspects statiques et dynamiques sont compatibles, ainsi java et C++ ont des systèmes de types à la fois statiques et dynamiques (le premier est fort et le second faible pour une grand part).

Types statiques

Les systèmes de types statiques tendent à améliorer la qualité du logiciel par un contrôle automatique du type des objets qui vonts évoluer au sein de l'application. Dans le cadre d'un système non statique, il faut s'appuyer sur des tests affin de s'assurer de la validité du type de tous les objets vivants dans l'application. Le problème est que la quantité de tests nécessaire est une fonction exponentielle du nombre d'instructions dans l'application, le taux de couverture du code diminue donc exponentiellement au temps à nombre de testeurs et vitesse de développement constants (refactoring mis à part). Les erreurs dûes au typage ne sont en général pas triviales.

Dans le cadre d'un système de types statique "simple", il est impossible d'abstraire, c'est pourquoi plusieurs solutions ont été développées :
note : vocabulaire "générique"/"paramétré" à stabiliser

Le typage faible

Le C offre la possibilité de manipuler des objets non complètement typés par la présence de pointeurs et la possibilité de transtyper abitrairement.
La fonction qsort() est un bon exemple d'abstraction en C, elle permet de trier n'importe quel tableau contenant n'importe quel type d'élément pourvu qu'on lui donne une relation d'ordre sur les éléments du tableau.

Les types génériques

Les types génériques permettent de laisser un "blanc" dans la définition d'un type, il n'est par exemple pas utile de préciser le type des éléments d'une liste si on désire simplement mesurer sa longueur.

Objective Caml utilise la notation 'a (prononciation : "quote a"), a désignera par la suite le type laissé libre dans tout le reste de la définition. La signature de la fonction qui compte le nombre d'éléments d'une liste est : val length : 'a list -> int , on voit donc que c'est une fonction qui prend un 'a list (une liste de n'importe quoi mais tous les éléments sont du même type car 'a ne désigne qu'un seul type à la fois) et renvoie un entier.
Cette notation est compatible avec le typage fort, car l'introduction de la notation "quote" vous interdit de supposer quoi que se soit sur le type désigné donc vous ne pourrez rien faire (sauf cas exceptionnels) de l'objet qui est derrière hormis les opérations applicables à tous les types telles que l'identité ==.

Ce mécanisme ne brise pas l'aspect fort du typage, car lors de la vérification, chaque occurence de 'a sera remplacé par le type réellement utilisé dans le code d'utilisation. Ainsi, les contraintes de types sont connues de bout-en-bout.
Il est à noter qu'on peut utiliser plusieur types inconnus dans le même type composé, par convention on les appelle 'a, 'b, 'c ... pour la simple raison qu'on ne sait rien dessus. Cette notation est utilisée principalement dans les langages de la famille ML, Haskell mais on retrouve régulièrement ce principe dans les langages fonctionnels (typés statiquement bien sur).

Les types paramétrés

Ces types possèdent des paramètres dans leur définition, leur conférant ainsi un aspect "réglable", il sont différents des types génériques car on peut utiliser les objets typés par les paramètres car une partie de leurs propriétés sont imposées.

Cette définition est relativement vague car elle permet d'inclure tant les templates C++ (ou bien les functor d'Objective Caml) où les paramètres sont des types et les "GENERIC" de VHDL où les paramètres sont des valeurs et le type des paramètres est fixe. Dans les 2 cas, les types des paramètres sont connus.

Bien que pas très "pur", le système des tableaux de java est à mettre dans cette catégorie, il permet de créer des tableaux tout en forçant le type des éléments.

Types non statiques

Les langages dont le système de types est uniquement dynamique (LISP, Smalltalk ...) offrent généralement un développement plus rapide et plus "convivial". Ils ne peuvent pas refuser de compiler pour des raisons de compatibilité de types.
Ils possèdent naturellement de bonnes capacités d'abstraction (généricité) car n'importe quel objet peut se trouver n'importe où, en particulier dans une variable et les fonctions peuvent retourner n'importe quoi. Il n'y a donc pas besoin d'ajouter de structure particulière au langage pour la généricité.
Leur principal défaut est leur manque de fiabilité, la probabilité d'envoyer un message à nil est assez forte dès que l'application devient un peu conséquente. Cependant, ils sont plus faciles à tester car les tests sont plus rapides à écrire.
L'ordre d'écriture des modules n'est généralement pas important, seule leur présence à l'exécution est nécessaire.
L'absence de types à la compilation est à l'origine de l'absence de surcharge dans les langages objets, on peut cependant la remplacer par un dispatching de méthodes (bien que ce soit plus lourd).


Hiérarchie de types

La notion de hiérarchie de types exprime simplement l'inclusion ensembliste injectée dans la définition donnée au début. Elle permet de décrire les objets présent dans l'application avec une granularité fine.

Ainsi, un type b est dérivé du type a si l'ensemble définit par b est inclus dans l'ensemble défini par a ont dit aussi que b est un sous-type de a. Ceci veut dire que partout où un objet de type a est attendu, un objet de type b peut s'y trouver. C'est le principe de substitution de Liskov. Attention car dériver de cette règle est très facile (Rectangle/Carré) et le problème n'est souvent constaté que très tard.

Dans les systèmes à typage statique, la dérivation d'un type à partir d'un type existant doit impérativement se faire avec la notation fournie à cette fin par le langage. Ainsi, des types qui seront inclus au sens mathématique mais qui n'auront pas été déclarés avec la bonne notation seront vus comme indépendants par le compilateur.

classes, interfaces

Ce terme vient de la classification des objets par leur comportement, le concept a été retourné pour définir le comportement de toute une classe d'objets d'un coup.
Une interface (au sens objet en général, pas au sens java) est un ensemble d'opération supportées par un objet, c'est le type au sens programmation orientée objets.
Une classe peut implanter plusieurs interfaces (E. Gamma et al. "Design Pattern", p.16), ainsi un humain peut compter et possède un volume, ce sont 2 caractéristiques différentes qui méritent d'être séparées.

Un objet peut donc posséder plusieurs types simultanément (les super-types de ses interfaces et ces dernières pour être précis). Ceci ne veut pas dire qu'une variable (ou une fonction) peut posséder plusiseurs types d'un coup, une varible possède un unique type. Par contre un objet peut être référencé par des varibles types différents (éventuellement simultanément) à condition qu'il possède bien les types appropriés.
Un petit exemple java :
 public class A implements I,J {
     public A() {}
 }
plus loin :
 A a_inst = new A();
 I i_inst = a_inst;
 J j_inst = a_inst;

on a ainsi 3 visions différentes du même objet, dont 2 complètement différentes : i_inst et j_inst n'ont absolument aucune méthode en commun.

Dans les langages possédant l'héritage multiple (C++ et Eiffel), les interfaces sont généralement décrites par des classes virtuelles héritées par les classes qui les implantent.

types génériques (2)

La présence d'une hiérarchie de types, permet intrinsèquement une certaine généricité, typer une variable par une classe dans un bout de code, permet de réutiliser ce bout de code avec dans la variable n'importe quel objet dont le type est une sous-classe de la classe prévue.
Les langages supportant la notion d'interface (C++, java, Eiffel par ex.) offrent une généricité encore plus grande puisque on peut typer directement par une interface, s'affranchissant directement des classes.
Cepandant, ce n'est pas suffisant, prenons un petit example java avec des collections (piqué ici) :
import java.util.ArrayList;
import java.util.Iterator;

public abstract class Forme {
  abstract double getAire(); // à définir
  void showAire() { 
    System.out.println("aire = "+getAire());}
}

public class Rectangle extends Forme{ ... }

public class Ellipse extends Forme { ... }

public class TestForme {
  public static void main(String[] args) {
   Forme r1 = new Rectangle(6,10);
   Forme r2 = new Rectangle(5,10);
   Forme e = new Ellipse(3,5);
   ArrayList liste = new ArrayList();
   liste.add(r1);
   liste.add(r2);
   liste.add(1,e);  // on a r1, e, r2
   for (Iterator it = liste.iterator(); it.hasNext();)
     ((Forme) it.next()).showAire();
 }  
}
Le problème est la présence du cast ; il est nécessaire car une collection ne peut contenir que des Object, la classe de plus haut niveau en java, pour utiliser un objet en sortie de collection, il faut donc lui redonner un type (Forme) permettant de lui envoyer les messages souhaités (en l'ocurence showAire()). Comme le type à utiliser (Forme) est sous-type de Object il faut donc parcourir la hiérarchie à l'envers, cette manoeuvre est contrôlée à l'exécution et est suceptible de déclencher une exception. Or on aurait très bien pu imaginer utiliser une collection ne pouvant contenir que des objets de type Forme ce que java ne permet pas sans redéfinir la classe ArrayList.
C'est pourquoi il existe des types paramétrés par un autre type dans certains langages objets (C++ par ex.). Paramétrer une collection par une classe (ou une interface) permet de s'assurer à la compilation du type des objets contenus et ainsi d'améliorer la qualité du code (car il n'y a pas la rupture que représente le cast dans le chemin de la vérification des types). Ce paramétrage est bien-entendu compatible avec l'utilisation de la hiérarchie des types.
Un petit exemple de paramétrage d'une collection en C++ (piqué ici ):

void listWords(istream& in, ostream& out)
{
        string s;
        vector<string> v;

        while (!in.eof() && in >> s)
                v.push_back(s);

        sort(v.begin(), v.end());

        vector<string>::iterator e 
                = unique(v.begin(), v.end());   

        for (vector<string>::iterator b = v.begin();
                b != e;
                b++) {
                out << *b << endl;
        }
}
on constate ici que l'élément n'est pas casté à la sortie de la collection.

contraintes sur les types génériques

Il existe des cas où la généricité et l'héritage ne sont pas suffisants, on pose alors des contraintes sur le type qu'on accepte en paramètre. Ainsi, les langages Eiffel et Objective Caml offrent-ils se type de contrainte, vous pouvez exiger que le type passé en paramètre possède une interface particulière sans en forcer le type.
Un petit exemple Eiffel :
 class interface DICTIONARY[V,K->HASHABLE]
 ...
 end of DICTIONARY[V,K->HASHABLE]
qui se lit : "La classe DICTIONARY est paramétrée par 2 inteface dont la deuxième comprend au moins l'interface HASHABLE". Ceci est utile afin d'insérer les valeurs dans la table de hachage.

Un exemple en Objective Caml :
class point (x_init : int) (y_init : int) =
   object
     val mutable x = x_init
     val mutable y = y_init
     method get_x = x
     method get_y = y
     method move v = x - x + v#get_x ; 
                     y - y + v#get_y
   end;;

class ['a] circle (c : 'a) =
   object 
     constraint 'a = #point
     val mutable center = c
     method center = center
     method set_center c = center - c
     method get_center = c
     method move = center#move
   end;;
qui se lit : "La classe circle est paramétrée par un type qui possède au moins l'interface de point". Ceci afin de s'assurer que le centre du cercle est un point, qu'il soit chantant ou dansant étant sans importance ici.

Quelle est la différence avec typer simplement ce paramètre par une interface ?
A la conception de la classe, rien ne change, vous avez accès aux interfaces que vous avez contraint. Par contre, la différence se situe à "l'extérieur" (une instance) de votre classe, ainsi, si vous instanciez un cercle avec pour centre un point chantant, il ne va pas "transformer" votre point chantant en point normal au cours de son séjour dans l'instance (comme si vou aviez décrêté que le centre d'un cercle est un point), il va réelement rester chantant. Ainsi, la notaion 'a aura bien "transmis" l'interface complète de votre objet comme vous vous y attendiez. Mais la contrainte vous a empêché de donner pour centre à votre cercle une carotte. Le principe est le même avec les types contraints de Eiffel, à l'intérieur de la classe vous n'avez accès qu'au type contraint ; à l'extérieur, au type que vous lui avez donné comme un type paramétré ou générique simple.

version "typage classique" :
class circle_normal (c : point) =
   object 
     val mutable center = c
     method center = center
     method set_center c = center - c
     method get_center = c
     method move = center#move
   end;;
la méthode get_center renvoie bien une instance point
utilisons un peu cette classe :
 let a1 = 
     let p = new point 10 10 in
     let c = new cicle_normal p in
     c#get_center;;
on trouve a1 est de type point ; jusqu'ici rien de particulier.

sous-classons point :
class point_chantant (x_init : int) (y_init : int) (c_init : string) =
   object
     inherit point x_init y_init
     val chanson = c_init
     method get_chanson = chanson
   end;;

faisons le même test avec point_chantant :
 let a2 = 
     let p = new point_chantant 10 10 "La traviata" in
     let c = new cicle_normal p in
     c#get_center;;
on trouve à nouveau que a2 est de type point, c'est logique, la méthode get_center a pour type de retour point.
utilisons la classe circle définie plus haut :
class ['a] circle (c : 'a) =
   object 
     constraint 'a = #point
     val mutable center = c
     method center = center
     method set_center c = center - c
     method get_center = c
     method move = center#move
   end;;
la méthode get_center renvoie cette fois-ci un objet de type 'a qui est le type du centre passé en paramètre.

 let a3 = 
     let p = new point 10 10 in
     let c = new cicle p in
     c#get_center;;
ici a3 est de type point car p est de type point.

réutilisons le point_chantant :
 let a4 = 
     let p = new point_chantant 10 10 "Le sud" in
     let c = new cicle p in
     c#get_center;;
cette fois-ci a4 est de type point_chantant car p était de type point_chantant.
La contrainte a simplement vérifié que le type de p possède bien l'interface point.


covariance/contravariance

contrats


NULL dans les types statiques

Le principe de typage statique est que l'objet référencé par une variable est de type connu. Il existe cependant un exception : la valeur NULL (ou nil), elle transcende le système de types en étant affectable à n'importe quelle variable quelque soit sont type. Ceci est un défaut majeur dans les langages typés statiquement possédant NULL car ceci se traduit par une exception (souvent non attrapée car une reprise sur cette erreur est très compliquée) ou pire, pour les langages faiblement typés, un accès mémoire invalide.
Plusieurs solutions existent, la plus simple est de ne pas mettre NULL dans le langage. Cette solution est adoptée par certains langages fonctionels car ceux-ci n'ont pas la notion d'affectation de variable mais la notion d'attachement de noms à des valeurs. L'attachement est différent en ce sens qu'il est définitif, le même nom référence toujours la même valeur de sa création jusqu'à sa sortie de zone de validité. La notion de varibale non-encore affectée n'existant pas, le problème de la valeur par défaut non plus.
La seconde n'est pas vraiment une solution mais un constat simple : les langages dont le typage est uniquement dynamique ne peuvent rien garantir sur le type du contenu d'une variable, donc la présence d'un entier, d'une vache ou de NULL n'a de toute façon rien de choquant, on sait qu'on risque de trouver n'importe quoi n'importe où. Dans ces langages, NULL pourraît même jouer en partie le rôle de unit dans les langages fonctionnels ou de la valeur de void en C. La présence de NULL ne relève donc pas d'un régime dérogatoire (en Smalltalk par ex. c'est une simple variable globale utilisée comme singleton).

Les ovnis du typage


Les assembleurs

Les différents assembleurs ne possèdent pas de système de type réel, c'est l'instruction utilisée qui suppose le type de ce qu'elle accède. Il n'y a bien entendu aucune vérification de type dans ces circonstances. Seules les adresses mémoire sont vérifiées : elle doivent se situer dans un endroit où l'opération à effectuer est valide, à condition de la plateforme possède une unité de gestion
de la mémoire (MMU).

Forth

Ce langage, le premier langage de la 4ème génération ("haut niveau"), agit à la manière d'un assembleur, l'instruction ne vérifie pas ce qui est posé sur la pile au moment où elle l'accède. On peut cependant imaginer un système de types entièrement dynamique, il vérifierait le type de l'élément avant d'agir dessus à la manière de smalltalk ou lisp. Je n'ai jamais entendu parler d'une concrètisation de ce système.


Nicolas Raynaud

Link to this Page