Précédent Index Suivant

Sous-typage et polymorphisme d'inclusion

Le sous-typage est la possibilité pour un objet d'un certain type d'être considéré et utilisé comme un objet d'un autre type. Un type d'objet ot2 pourra être un sous-type de ot1 si
  1. il possède au moins toutes les méthodes de ot1,
  2. le type de chaque méthode de ot2 présente dans ot1 est sous-type de celle de ot1.
La relation de sous-typage n'a de sens qu'entre objets. Elle ne devra donc être exprimée qu'entre objets. De plus, la relation de sous-typage devra toujours être explicite. On peut indiquer soit qu'un type est sous-type d'un autre, soit qu'un objet doit être considéré comme objet d'un sur-type.

Syntaxe


(nom : sous_type :> sur_type)
(nom :> sur_type)

Exemple

Ainsi, on pourra indiquer qu'une instance de colored_point peut être utilisée comme une instance de point :

# let pc = new colored_point (4,5) "blanc";;
val pc : colored_point = <obj>
# let p1 = (pc : colored_point :> point);;
val p1 : point = <obj>
# let p2 = (pc :> point);;
val p2 : point = <obj>


Bien que connu comme un point, p1 n'en reste pas moins un point coloré et l'envoi de la méthode to_string déclenchera l'exécution de la méthode attachée aux points colorés :

# p1#to_string();;
- : string = "( 4, 5) de couleur blanc"


On peut ainsi construire des listes contenant à la fois des points et des points colorés :

# let l = [new point (1,2) ; p1] ;;
val l : point list = [<obj>; <obj>]
# List.iter (fun x -> x#print(); print_newline()) l;;
( 1, 2)
( 4, 5) de couleur blanc
- : unit = ()


Bien entendu, les manipulations que l'on peut faire sur les objets d'une telle liste sont restreintes à celles autorisées sur les points.

# p1#get_color () ;;
Characters 1-3:
This expression has type point
It has no method get_color


Cette combinaison de liaison tardive et sous-typage autorise une nouvelle forme de polymorphisme : le polymorphisme d'inclusion. C'est-à-dire la possibilité de manipuler des valeurs de n'importe quel type en relation de sous-typage avec le type attendu. Dès lors, l'information de typage statique garantit que l'envoi d'un message trouvera toujours la méthode correspondante, mais le comportement de cette méthode dépendra de l'objet receveur effectif.

Sous-typage n'est pas héritage

Le sous-typage est une notion différente de celle d'héritage. Il y a deux arguments principaux à cela.

Le premier est qu'une instance d'une classe c2 peut avoir comme type un sous-type du type objet c1 sans que c2 soit sous-classe de c1. En effet, on aurait pu définir la classe colored_point de manière indépendante de la classe point et contraindre le type de l'une de ses instances en type objet point.

Le second est qu'il est aussi possible d'avoir un héritage de classes sans pouvoir faire du sous-typage entre instances de ces classes. L'exemple ci-dessous illustre ce second point. Il utilise la possibilité de définir une méthode abstraite prenant en argument une instance (non encore déterminée) de la classe en cours de définition. Dans notre exemple, c'est la méthode eq de la classe equal.

# class virtual equal () =
object(self:'a)
method virtual eq : 'a -> bool
end;;
class virtual equal : unit -> object ('a) method virtual eq : 'a -> bool end
# class c1 (x0:int) =
object(self)
inherit equal ()
val x = x0
method get_x = x
method eq o = (self#get_x = o#get_x)
end;;
class c1 :
int ->
object ('a) val x : int method eq : 'a -> bool method get_x : int end
# class c2 (x0:int) (y0:int) =
object(self)
inherit equal ()
inherit c1 x0
val y = y0
method get_y = y
method eq o = (self#get_x = o#get_x) && (self#get_y = o#get_y)
end;;
class c2 :
int ->
int ->
object ('a)
val x : int
val y : int
method eq : 'a -> bool
method get_x : int
method get_y : int
end


On ne peut pas forcer une instance de c2 à être du type des instances de c1 :

# let a = ((new c2 0 0) :> c1) ;;
Characters 11-21:
This expression cannot be coerced to type
c1 = < eq : c1 -> bool; get_x : int >;
it has type c2 = < eq : c2 -> bool; get_x : int; get_y : int >
but is here used with type < eq : c1 -> bool; get_x : int; get_y : int >
Type c2 = < eq : c2 -> bool; get_x : int; get_y : int >
is not compatible with type c1 = < eq : c1 -> bool; get_x : int >
Only the first object type has a method get_y


L'incompatibilité entre les types c1 et c2 vient en fait de ce que le type de eq dans c2 n'est pas un sous-type du type de eq dans c1.

Pour montrer qu'il est bon qu'il en soit ainsi, comme dans nos bons vieux devoirs de mathématiques : << raisonnons par l'absurde >>. Soient o1 une instance de c1 et o21 une instance de c2 sous-typée en c1. Si nous supposons que le type de eq dans c2 est un sous-type du type de eq dans c1 alors l'expression o21#eq(o1) est correctement typée (o21 et o1 sont toutes deux de type c1). Cependant, à l'exécution, c'est la méthode eq de c2 qui est déclenchée (puisque o21 est une instance de c2). Cette méthode va donc tenter d'envoyer le message get_y à o1 qui ne possède pas une telle méthode !

On aurait donc un système de type qui ne remplirait plus son office. C'est pourquoi la relation de sous-typage entre types fonctionnels doit être définie moins naïvement. C'est ce que nous proposons au paragraphe suivant.

Formalisation

Sous-typage entre objets
Soient t=<m1:t1; ... mn: tn> et t'=<m1:s1 ; ... ; mn:sn; mn+1:sn+1; etc...> on dit que t' est un sous-type de t, noté t' £ t, si et seulement si si £ ti pour i Î {1,...,n}.

Appel de fonction
Si f : t ® s, si a:t' et t' £ t alors (f a) est bien typé et a le type s.

Intuitivement, une fonction f qui attend un argument de type t peut recevoir sans danger un argument d'un sous-type t' de t.

Sous-typage des types fonctionnels
Le type t'® s' est un sous type de t® s, noté t'® s' £ t® s, si et seulement si
s'£ s et t £ t'
La relation s'£ s est appelée covariance et la relation t £ t' est appelée contravariance. Cette relation a priori surprenante entre les types fonctionnels peut facilement être justifiée dans le cadre des programmes objets avec liaison dynamique.

Supposons deux classes c1 et c2 possédant toutes deux une méthode m. La méthode m a le type t1® s1 dans c1 et le type t2® s2 dans c2. Pour plus de lisibilité, notons m(1) la méthode m de c1 et m(2) celle de c2. Supposons enfin c2£ c1, c'est à dire t2® s2 £ t1® s1, et voyons d'où viennent les relations de covariance et de contravariance sur un petit exemple.

Soit g : s1 ® a, posons h (o:c1) (x:t1) = g(o#m(x))

covariance
la fonction h attend comme premier argument un objet de type c1, comme c2£ c1 on peut lui passer un objet de type c2. La méthode invoquée par o#m(x) est alors m(2) qui retourne une valeur de type s2. Comme cette valeur est passée à g qui attend un argument de type s1, il faut bien que s2£ s1.
contravariance
la fonction h attend, comme second argument, une valeur de type t1. Si, comme précédemment, nous passons à h un premier argument de type c2, la méthode m(2) est invoquée et elle attend un argument de type t2. Il faut donc qu'impérativement t1£ t2.

Polymorphisme d'inclusion

On appelle << polymorphisme >> la possibilité d'appliquer une fonction à des arguments de n'importe quelle << forme >> (type) ou d'envoyer un message à des objets de formes différentes.

Dans le cadre du noyau fonctionnel/impératif du langage, nous avons déjà rencontré le polymorphisme paramétrique qui permet d'appliquer une fonction à des arguments de n'importe quel type. Le paramètre polymorphe de la fonction a un type contenant une variable de type. Une fonction polymorphe est une fonction qui exécutera le même code pour les différents types de paramètres. Pour cela elle n'explore pas la structure de l'argument.

La relation de sous-typage utilisée avec la liaison retardée introduit un nouveau genre de polymorphisme pour les méthodes : le polymorphisme d'inclusion. Celui-ci autorise l'envoi d'un même message, à des instances de types différents, si celles-ci ont été contraintes vers le même sur-type. On construit une liste de points, dont certaines valeurs sont en fait des points colorés (vus comme des points). Le même envoi de message entraîne l'exécution de méthodes différentes, sélectionnées par l'instance réceptrice. Ce polymorphisme est appelé d'inclusion car il accepte l'envoi d'un message, contenu dans la classe c, sur toute instance d'une classe sc, sous-type de c (sc :> c) qui est contrainte en c. On obtient alors un envoi de message polymorphe sur toutes les classes de l'arbre des sous-types de c. À la différence du polymorphisme paramétrique le code exécuté peut être différent pour ces instances.

Les deux formes de polymorphisme peuvent être utilisées conjointement grâce aux classes paramétrées.

Égalité entre objets

Nous pouvons maintenant expliquer le comportement surprenant de l'égalité structurelle entre objets présentée à la page ??. Un objet est égal structurellement à un autre uniquement s'il est physiquement égal à celui-ci.

# let p1 = new point (1,2);;
val p1 : point = <obj>
# p1 = new point (1,2);;
- : bool = false
# p1 = p1;;
- : bool = true


Cela provient de la relation de sous-typage. En effet une instance o2 d'une classe sc, sous-type de c, contrainte en c peut être comparée à une instance o1 de la classe c. Si les champs communs à ces deux instances sont égaux alors les deux objets seraient considérés comme égaux, ce qui est faux du point de vue structurel car o2 peut avoir des champs supplémentaires. Pour cela Objective CAML considère que deux objets physiquement différents sont structurellement différents.

# let pc1 = new colored_point (1,2) "rouge";;
val pc1 : colored_point = <obj>
# let q = (pc1 :> point);;
val q : point = <obj>
# p1 = q;;
- : bool = false
C'est une vision restrictive de l'égalité qui garantit qu'une réponse true n'est pas erronée; la réponse false ne garantit rien.


Précédent Index Suivant