Types et généricité
L'intérêt de la programmation objet vient d'une part de la
modélisation d'un problème par les relations d'agrégation et
d'héritage, mais aussi de la possibilité de réutilisation et de
modification de comportement des classes. L'extension objet d'Objective CAML
doit de surcroît conserver les propriétés de typage statique du langage.
Cette section présente les principaux traits facilitant cette
ouverture. Les classe abstraites permettent d'une part de factoriser
du code mais aussi de regrouper dans un même ``protocole de
communication'' ses sous-classes : une classe abstraite fixe les noms
et types des messages que peuvent recevoir leurs instances des ses
héritières. Cette dernière notion sera encore plus appréciée avec
l'héritage multiple.
La notion de type objet ouvert ou, plus simplement, de type
ouvert, définissant un minimum de méthodes requises,
va autoriser la manipulation d'instances par des fonctions
génériques. Il faudra cependant, pour cela, parfois préciser des
contraintes de types. Ce sera
indispensable avec les classes paramétrées qui reprennent la
généricité du polymorphisme paramétrique dans le cadre des
classes. Ce dernier trait de la couche objet d'Objective CAML lui permet
d'être vraiment générique.
Classes et méthodes abstraites
Les classes abstraites sont des classes dont certaines méthodes sont
déclarées mais ne possèdent pas de corps. Ces méthodes sont
dites alors abstraites. Il n'est pas possible d'instancier une classe
abstraite : new est interdit. On utilise le mot clef
virtual pour signifier l'abstraction d'une classe ou d'une
méthode.
Syntaxe
class virtual nom_classe = définition de la classe |
Syntaxe
method virtual nom_methode : type |
Une classe doit impérativement être déclarée abstraite dés
lors que l'une de ses méthodes l'est. Lorsqu'une une sous-classe,
d'une classe abstraite, redéfinit toutes les méthodes abstraites
de son ancêtre, alors elle peut devenir concrète, sinon elle doit
elle aussi être déclarée abstraite.
Nous voulons construire un ensemble d'objets affichables disposant
tous d'une méthode print qui affichera le contenu de
l'objet traduit en chaîne de caractères. Ces objets
devront donc posséder une méthode
to_string.
Nous définissons à cet effet la classe printable.
Suivant la nature des objets considérés, la chaîne à
construire pourra varier. La méthode to_string sera donc
abstraite dans la déclaration de printable.
Du coup, cette classe est également abstraite.
# class
virtual
printable
()
=
object(self)
method
virtual
to_string
:
unit
->
string
method
print
()
=
print_string
(self#to_string())
end
;;
class virtual printable :
unit ->
object
method print : unit -> unit
method virtual to_string : unit -> string
end
Notons que le caractère abstrait de la classe et de sa méthode
to_string est manifeste dans le type obtenu.
À partir de cette classe on cherche à définir la hiérarchie de
classes de la figure 2.4.
Figure 2.4 : Relations entre classes d'objets affichables
On redéfinit facilement les classes point,
colored_point et picture. Il suffit d'ajouter dans
leurs déclarations la ligne inherit printable () pour
les doter de la méthode print.
# let
p
=
new
point
(1
,
1
)
in
p#print()
;;
( 1, 1)- : unit = ()
# let
pc
=
new
colored_point
(2
,
2
)
"bleu"
in
pc#print()
;;
( 2, 2) de couleur bleu- : unit = ()
# let
t
=
new
picture
3
in
t#add
(new
point
(1
,
1
))
;
t#add
(new
point
(3
,
2
))
;
t#add
(new
point
(1
,
4
))
;
t#print()
;;
[ ( 1, 1) ( 3, 2) ( 1, 4)]- : unit = ()
La sous-classe rectangle suivante hérite de
printable et définit la méthode to_string.
Les variable d'instance llc et ruc désignent respectivement
le point en bas à gauche et le point en haut à droite du rectangle.
# class
rectangle
(p1,
p2)
=
object
inherit
printable
()
val
mutable
llc
=
(p1
:
point)
val
mutable
ruc
=
(p2
:
point)
method
to_string
()
=
"["
^
p1#to_string()
^
","
^
p2#to_string()
^
"]"
end
;;
class rectangle :
point * point ->
object
val mutable llc : point
val mutable ruc : point
method print : unit -> unit
method to_string : unit -> string
end
La classe rectangle hérite de la classe abstraite
printable, et donc récupère la méthode
print. Elle possède deux variables d'instances
point : les coins inférieur gauche (llc) et
supérieur droit (ruc) Sa méthode to_string envoie
le message to_string à ses variables d'instance point.
# let
r
=
new
rectangle
(new
point
(2
,
3
),
new
point
(4
,
5
));;
val r : rectangle = <obj>
# r#print();;
[( 2, 3),( 4, 5)]- : unit = ()
Classes, types et objets
Le type d'un objet est le type de ses méthodes. Par exemple le type
point, inféré lors de la déclaration de la classe
point, est une abréviation du type :
point =
< distance : unit -> float; get_x : int; get_y : int;
moveto : int * int -> unit; rmoveto : int * int -> unit;
to_string : unit -> string >
C'est un type objet fermé. C'est-à-dire que la totalité
des méthodes et des types associés qui le compose est déterminée ; il
n'y en a pas d'autre.
Le mécanisme d'inférence de types, lors d'une déclaration de
classe, calcule le type fermé associé à cette classe.
Types ouverts
L'envoi d'une message à un objet fait partie intégrante du
langage, on peut donc définir une fonction envoyant un message
quelconque à un objet dont le type est non précisé.
# let
f
x
=
x#get_x
;;
val f : < get_x : 'a; .. > -> 'a = <fun>
Le type inféré pour l'argument de f est un type objet,
puisque l'on envoie un message à x, mais ce type objet est
ouvert. Le paramètre x de la fonction f
doit au moins posséder une
méthode get_x. Comme le résultat de l'envoi de ce message n'est
pas utilisé dans la fonction f, son type est le plus général possible (c'est-à-dire un variable de type 'a).
Le mécanisme d'inférence de type laisse
donc la possibilité d'utiliser la fonction f avec n'importe
quel objet possédant une méthode get_x. L'ouverture du
type de x est signifiée par les deux points (..) du type
< get_x : 'a; .. >
# f
(new
point(2
,
3
))
;;
- : int = 2
# f
(new
colored_point(2
,
3
)
"vert emeraude"
)
;;
- : int = 2
# class
c
()
=
object
method
get_x
=
"J'ai une méthode get_x"
end
;;
class c : unit -> object method get_x : string end
# f
(new
c
())
;;
- : string = "J'ai une méthode get_x"
L'inférence de types pour les classes peut engendrer des types ouverts,
en particulier pour les valeurs initiales de construction des instances.
L'exemple suivant construit une classe couple,
dont les valeurs initiales a
et b possèdent une méthode to_string.
# class
couple
(a,
b)
=
object
val
p0
=
a
val
p1
=
b
method
to_string()
=
p0#to_string()
^
p1#to_string()
method
copy
()
=
new
couple
(p0,
p1)
end
;;
class couple :
(< to_string : unit -> string; .. > as 'a) *
(< to_string : unit -> string; .. > as 'b) ->
object
val p0 : 'a
val p1 : 'b
method copy : unit -> couple
method to_string : unit -> string
end
Les types de a et b sont deux types ouverts possédant
la méthode to_string. On note que ces deux types sont
considérés comme différents. Ils sont notés respectivement comme
as 'a et
as 'b. Les variables de
types 'a et 'b sont contraintes par le type ouvert engendré.
On désigne un type ouvert construit à partir d'un type fermé obj_type par la notation dièse suivante :
#obj_type qui représente toutes les méthodes du type obj_type suivies des deux points. Les types ouverts sont utilisés lors de coercion de types.
# let
f
x
=
(
x#get
:
unit
)
;;
val f : < get : unit; .. > -> unit = <fun>
# let
g
(
x
:
<
get:
'a
>
)
=
f
x
;
x#other
;;
Characters 33-34:
This expression has type < get : unit >
It has no method other
# let
g
(
x
:
<
get:
'a
;
..
>
)
=
f
x
;
x#other
;;
val g : < get : unit; other : 'a; .. > -> 'a = <fun>
# class
c
=
object
method
get
=
()
end
;;
class c : object method get : unit end
# let
g
(x:
c)
=
f
x
;
x#other
;;
Characters 20-21:
This expression has type c
It has no method other
# let
g
(x:#
c)
=
f
x
;
x#other
;;
val g : < get : unit; other : 'a; .. > -> 'a = <fun>
Coercion de types
Nous avons présenté au chapitre sur la programmation fonctionnelle,
page , comment contraindre une expression à
posséder un type plus précis que celui engendré par l'inférence. Les types <<objet>>, fermés ou ouverts, peuvent
être utilisés pour effectuer de telles coercions.
On peut vouloir a priori ouvrir le type d'un objet défini pour
lui appliquer une méthode à venir. On utilise alors une coercion
de type objet ouvert.
Syntaxe
(x:#type_objet)
On peut alors écrire
# let
g
(x
:
#point)
=
x#amess;;
val g :
< amess : 'a; distance : unit -> float; get_x : int; get_y : int;
moveto : int * int -> unit; print : unit -> unit;
rmoveto : int * int -> unit; to_string : unit -> string; .. > ->
'a = <fun>
où la coercion de type avec #point force x
à avoir au moins toutes les méthodes de point, et
l'envoi du message amess ajoute une méthode au type du
paramètre x.
Comme dans le reste du langage, l'extension objet d'Objective CAML offre un
typage statique obtenu par inférence. Lorsque ce mécanisme ne
dispose pas de l'information suffisante pour déterminer le type
d'une expression, il lui assigne une variable de type. Nous venons de
voir que ce phénomène est aussi valable pour le typage des
objets. Il peut engendrer parfois des situations de blocage que
l'utilisateur devra résoudre en donnant explicitement des
informations de type.
# class
a_point
p0
=
object
val
p
=
p0
method
to_string()
=
p#to_string()
end
;;
Characters 6-89:
Some type variables are unbound in this type:
class a_point :
(< to_string : unit -> 'b; .. > as 'a) ->
object val p : 'a method to_string : unit -> 'b end
The method to_string has type unit -> 'a where 'a is unbound
On sort de ce blocage en précisant que le paramètre p0
est de type #point.
# class
a_point
(p0
:
#point)
=
object
val
p
=
p0
method
to_string()
=
p#to_string()
end
;;
class a_point :
(#point as 'a) -> object val p : 'a method to_string : unit -> string end
Pour éviter de placer les coercions de types à divers endroits de la
déclaration d'une classe, on utilise la syntaxe suivante :
Syntaxe
constraint type_expr_1 = type_expr_2
L'exemple précédent peut se réécrit en indiquant que le paramètre
p0 est de type 'a, puis l'on pose une contrainte
de type sur la variable 'a.
# class
a_point
(p0
:
'a)
=
object
constraint
'a
=
#point
val
p
=
p0
method
to_string()
=
p#to_string()
end
;;
class a_point :
(#point as 'a) -> object val p : 'a method to_string : unit -> string end
Plusieurs contraintes de types peuvent être posées dans une déclaration de classe.
Warning
Un type ouvert ne peut pas apparaître comme type d'une méthode. |
Cette contrainte forte provient du fait qu'un type ouvert contient une
variable de type non encore instanciée provenant de la suite du
type. Comme on ne peut avoir une variable de type libre dans une
déclaration de type, une méthode contenant un tel type est rejetée par
l'inférence de type.
# class
b_point
p0
=
object
inherit
a_point
p0
method
get
=
p
end
;;
Characters 6-77:
Some type variables are unbound in this type:
class b_point :
(#point as 'a) ->
object val p : 'a method get : 'a method to_string : unit -> string end
The method get has type #point where .. is unbound
Le type de get est #point, type ouvert (..).
Le message d'erreur indique que le type dess méthodes, de ce type, n'est pas résolu.
Héritage et type de self
Il existe une exception pour l'apparition d'une variable de
type dans le type des méthodes dans le cas où cette variable de type est
celui de self. Prenons le cas d'une méthode d'égalité qui teste si
un point est égal à un autre.
# class
point_eq
(x,
y)
=
object
(self
:
'a)
inherit
point
(x,
y)
method
eq
(p:
'a)
=
(self#get_x
=
p#get_x)
&&
(self#get_y
=
p#get_y)
end
;;
class point_eq :
int * int ->
object ('a)
val mutable x : int
val mutable y : int
method distance : unit -> float
method eq : 'a -> bool
method get_x : int
method get_y : int
method moveto : int * int -> unit
method print : unit -> unit
method rmoveto : int * int -> unit
method to_string : unit -> string
end
Le type de la méthode eq est 'a -> bool, mais la variable
de type est liée au type 'a de l'instance à sa construction.
On peut hériter de la classe point_eq et redéfinir
la méthode eq dont le type est toujours paramétré par
le type de l'instance.
# class
colored_point_eq
(a,
b)
c
=
object
(self
:
'a)
inherit
point_eq
(a,
b)
as
super
val
c
=
(c:
string)
method
get_c
=
c
method
eq
(pc
:
'a)
=
(self#get_x
=
pc#get_x)
&&
(self#get_y
=
pc#get_y)
&&
(self#get_c
=
pc#get_c)
end
;;
class colored_point_eq :
int * int ->
string ->
object ('a)
val c : string
val mutable x : int
val mutable y : int
method distance : unit -> float
method eq : 'a -> bool
method get_c : string
method get_x : int
method get_y : int
method moveto : int * int -> unit
method print : unit -> unit
method rmoveto : int * int -> unit
method to_string : unit -> string
end
La méthode eq, de la classe colored_point_eq, est
toujours du type 'a -> bool, mais la variable 'a désigne
le type d'une instance de la classe colored_point_eq. La
définition de eq de la classe colored_point_eq
masque celle héritée. Les méthodes contenant le type de l'instance
dans leur type sont appelées méthodes binaires. Elles créeront
des limitations à la relation de sous-typage décrite à la page X.
Héritage multiple
L'héritage multiple permet d'hériter des champs de données et
des méthodes de plusieurs classes. En cas de noms de champs ou de
méthodes identiques, seulement la dernière déclaration, dans
l'ordre de la déclaration de l'héritage, sera conservée. Néanmoins,
on peut indiquer une méthode d'une des classes ancêtres, à la manière de l'héritage simple, en associant des noms différents à chaque classe dont on hérite.
Cela n'est pas valable pour les variables d'instance. Si une classe
héritée masque une variable d'instance d'une classe précédemment
héritée, cette dernière variable n'est plus accessible directement.
Les différentes classes héritées n'ont pas forcément de liens
d'héritage entre elles.
L'intérêt de l'héritage multiple est d'augmenter la
réutilisabilité des classes.
On définit la classe abstraite geometric_object qui
déclare deux méthodes virtuelles compute_area et
compute_circ pour le calcul de la surface et du
périmètre.
# class
virtual
geometric_object
()
=
object
method
virtual
compute_area
:
unit
->
float
method
virtual
compute_circ
:
unit
->
float
end;;
On redéfinit alors la classe rectangle de la manière
suivante :
# class
rectangle_bis
((p1,
p2)
:
'a)
=
object
constraint
'a
=
point
*
point
inherit
printable
()
inherit
geometric_object
()
val
mutable
llc
=
p1
val
mutable
ruc
=
p2
method
to_string
()
=
"["
^
p1#to_string()^
","
^
p2#to_string()^
"]"
method
compute_area()
=
float
(
abs(ruc#get_x
-
llc#get_x)
*
abs(ruc#get_x
-
llc#get_y))
method
compute_circ()
=
float
(
(abs(ruc#get_x
-
llc#get_x)
+
abs(ruc#get_x
-
llc#get_y))
*
2
)
end;;
class rectangle_bis :
point * point ->
object
val mutable llc : point
val mutable ruc : point
method compute_area : unit -> float
method compute_circ : unit -> float
method print : unit -> unit
method to_string : unit -> string
end
Cette implantation de classes respecte le graphe d'héritage de la figure 2.5.
Figure 2.5 : héritage multiple
Pour ne pas avoir à réécrire la méthode de la classe rectangle,
on peut hériter
directement de rectangle comme le montre le schéma de la
figure 2.6.
Figure 2.6 : héritage multiple
Dans ce cas là, seules les méthodes abstraites de la classe
abstraite geometric_object doivent être définies dans
rectangle_ter.
# class
rectangle_ter
(p2
:
'a)
=
object
constraint
'a
=
point
*
point
inherit
rectangle
p2
inherit
geometric_object
()
method
compute_area()
=
float
(
abs(ruc#get_x
-
llc#get_x)
*
abs(ruc#get_x
-
llc#get_y))
method
compute_circ()
=
float
(
(abs(ruc#get_x
-
llc#get_x)
+
abs(ruc#get_x
-
llc#get_y))
*
2
)
end;;
Toujours dans la même veine, les développements de la hiérarchie
graphical_object et geometric_object auraient
pu être séparés jusqu'au moment il devenait utile d'avoir une
classe possèdant les deux comportements. Le schéma de la figure
2.7 montrent les relations ainsi définies.
Figure 2.7 : héritage multiple
Si on suppose que les classes rectangle_graph et
rectangle_geo définissent des variables d'instances pour
les coins d'un rectangle, on se retrouve dans la classe
rectangle avec quatre points (deux par coin).
class rectangle (p1,p2) =
inherit rectangle_graph (p1,p2) as super_graph
inherit rectangle_geo (p1,p2) as super_geo
end;;
Dans le cas où des méthodes de même type existent dans les deux
classes rectangle_..., alors seule la dernière est
visible. Néanmoins, grâce au nommage des classes ancêtres
(super_...), il est toujours possible d'invoquer une
méthode de l'une ou l'autre des ancêtres.
L'héritage multiple permet donc une factorisation du code puisqu'il
permet de récupérer facilement les méthodes déjà écrites de
différentes classes ancêtres. Le prix à payer est la taille des
objets construits plus gros que nécessaires : duplication de champs,
héritage de champs inutiles à une application donnée, etc...
De plus, en cas de duplication, comme dans notre dernier exemple, il
faut établir manuellement la communication entre ces champs (mise
à jour, etc...). Dans le dernier exemple sur la classe rectangle,
on obtient les variables d'instance de la classe rectangle_graph
et rectangle_geo. Si l'une de ces classes possède une méthode qui modifie
ces variables (comme un facteur d'échelle) alors il est nécessaire de
répercuter ces modifications aux variables héritées de l'autre classe.
Cette trop lourde communication entre variables d'instance héritées souligne
souvent une mauvaise modélisation du problème posé.
Classes paramétrées
Les classes paramétrées permettent d'utiliser le polymorphisme
paramétrique d'Objective CAML dans les classes. Une déclaration de classe
peut être paramétrée par des variables de types, comme dans une
déclaration de types d'Objective CAML. Cela offre de nouvelles
possibilités de généricité, pour la réutilisation du code,
et s'intègre dans le typage à la ML quand l'inférence de type
produit des types paramétrés.
La syntaxe diffère légèrement de la déclarations de types paramétrés.
les paramètres de type sont entre crochets.
Syntaxe
class ['a,'b ] nom_classe ...
Néanmoins le type Objective CAML est comme à l'habitude ('a,'b)
nom_classe.
Par exemple, si l'on désire construire une classe pair pour
décrire les paires, une solution naïve consiste à poser :
# class
pair
x0
y0
=
object
val
x
=
x0
val
y
=
y0
method
fst
=
x
method
snd
=
y
end
;;
Characters 6-106:
Some type variables are unbound in this type:
class pair :
'a ->
'b -> object val x : 'a val y : 'b method fst : 'a method snd : 'b end
The method fst has type 'a where 'a is unbound
On retrouve l'erreur de typage évoquée à la définition de la classe a_point
(page X).
Le message d'erreur indique que la variable de type 'a
assignée au paramètre x0 (et donc à x et
fst) n'est pas liée. Pour obtenir un typage correct, comme
dans le cas des types paramétrés, il faut paramétrer la classe
pair avec deux variables de type et forcer le type des
paramétres de construction x0 et y0 :
# class
[
'a,
'b]
pair
(x0:
'a)
(y0:
'b)
=
object
val
x
=
x0
val
y
=
y0
method
fst
=
x
method
snd
=
y
end
;;
class ['a, 'b] pair :
'a ->
'b -> object val x : 'a val y : 'b method fst : 'a method snd : 'b end
L'affichage de l'inférence de types indique une interface de classe
paramétrée par les variables de type 'a et 'b.
Quand on construit une valeur d'une classe paramétrée, les paramètres de type
sont alors instanciés.
# let
p
=
new
pair
2
'X'
;;
val p : (int, char) pair = <obj>
# p#fst;;
- : int = 2
# let
q
=
new
pair
3
.
1
2
true;;
val q : (float, bool) pair = <obj>
# q#snd;;
- : bool = true
Remarque
Dans les déclarations de classe, les paramètres de type sont notés entre crochets,
alors qu'au niveau de l'affichage des types, ils se retrouvent entre parenthèses. |
Héritage de classes paramétrées
L'héritage d'une classe paramétrée doit indiquer les paramètres de cette classe.
On définit une classe acc_pair, qui hérite de ('a,'b) pair
et ajoute deux méthodes d'accès aux champs , get1 et get2,
# class
[
'a,
'b]
acc_pair
(x0
:
'a)
(y0
:
'b)
=
object
inherit
[
'a,
'b]
pair
x0
y0
method
get1
z
=
if
x
=
z
then
y
else
raise
Not_found
method
get2
z
=
if
y
=
z
then
x
else
raise
Not_found
end;;
class ['a, 'b] acc_pair :
'a ->
'b ->
object
val x : 'a
val y : 'b
method fst : 'a
method get1 : 'a -> 'b
method get2 : 'b -> 'a
method snd : 'b
end
# let
p
=
new
acc_pair
3
true;;
val p : (int, bool) acc_pair = <obj>
# p#get1
3
;;
- : bool = true
On peut préciser les paramètres de type de la classe paramétrée héritée,
par exemple pour une paire de points.
# class
pair_point
(p1,
p2)
=
object
inherit
[
point,
point]
pair
p1
p2
end;;
class pair_point :
point * point ->
object
val x : point
val y : point
method fst : point
method snd : point
end
La classe pair_point n'a plus besoin de paramètres de type, car les
paramètres 'a et 'b sont complètement déterminés.
Pour construire des paires d'objets affichables, c'est-à-dire possédant une
méthode print, on réutilise la classe abstraite
printable (voir page X), puis
on définit la
classe printable_paire qui hérite de pair.
# class
printable_pair
(x0
)
(y0
)
=
object
inherit
[
printable,
printable]
acc_pair
x0
y0
method
print
()
=
x#print();
y#print
()
end;;
Cette implantation permet effectivement de construire des paires d'instances
de printable, mais ne peut pas être utilisée pour des objets
d'une autre classe et possédant une méthode print.
Un premier essai est d'ouvrir le type printable qui
paramétrise l'héritage.
# class
printable_pair
(x0
)
(y0
)
=
object
inherit
[
#printable,
#printable
]
acc_pair
x0
y0
method
print
()
=
x#print();
y#print
()
end;;
Characters 6-149:
Some type variables are unbound in this type:
class printable_pair :
(#printable as 'a) ->
(#printable as 'b) ->
object
val x : 'a
val y : 'b
method fst : 'a
method get1 : 'a -> 'b
method get2 : 'b -> 'a
method print : unit -> unit
method snd : 'b
end
The method fst has type #printable where .. is unbound
Cet essai échoue car les méthodes fst et snd contiennent
un type ouvert.
Nous allons donc conserver les paramètres de type de la classe, tout en les
contraignant au type ouvert #printable.
# class
[
'a,
'b]
printable_pair
(x0
)
(y0
)
=
object
constraint
'a
=
#printable
constraint
'b
=
#printable
inherit
[
'a,
'b]
acc_pair
x0
y0
method
print
()
=
x#print();
y#print
()
end;;
class ['a, 'b] printable_pair :
'a ->
'b ->
object
constraint 'a = #printable
constraint 'b = #printable
val x : 'a
val y : 'b
method fst : 'a
method get1 : 'a -> 'b
method get2 : 'b -> 'a
method print : unit -> unit
method snd : 'b
end
On construit alors une paire affichable contenant un point et un
point coloré.
# let
pp
=
new
printable_pair
(new
point
(1
,
2
))
(new
colored_point
(3
,
4
)
"vert"
);;
val pp : (point, colored_point) printable_pair = <obj>
# pp#print();;
( 1, 2)( 3, 4) de couleur vert- : unit = ()
Classes paramétrées et typage
Une classe paramétrée est, du point de vue des types, un type paramétré.
Une valeur d'un tel type peut alors contenir des variables de type faibles.
# let
r
=
new
pair
[]
[];;
val r : ('_a list, '_b list) pair = <obj>
# r#fst;;
- : '_a list = []
# r#fst
=
[
1
;2
]
;;
- : bool = false
# r;;
- : (int list, '_a list) pair = <obj>
Une classe paramétrée est aussi vue comme un type objet fermé, donc rien n'empche
de l'utiliser aussi comme type ouvert avec la notation dièse.
# let
compare
(
x
:
('a,
'a)
#pair)
=
if
x#fst
=
x#fst
then
x#mess
else
x#mess2;;
val compare : < fst : 'a; mess : 'b; mess2 : 'b; snd : 'a; .. > -> 'b = <fun>
Ce qui nous amène à pouvoir construire des types paramétrés, contenant des variables
de type faibles, tout en étant des types objet ouverts.
# let
jolitype
x
(
y
:
('a,
'a)
#pair)
=
if
x
=
y#fst
then
y
else
y;;
val jolitype : 'a -> (('a, 'a) #pair as 'b) -> 'b = <fun>
Si on applique cette fonction à un seul paramètre, on obtient
une fermeture dont les variables de type deviennent faibles.
Un type ouvert, comme #pair, contient encore une partie à instancier
représentée par les deux points (..). En cela un type ouvert est un paramètre
de type dont une partie est déjà connue. Lors de l'affaiblissement, suite à
une application partielle, d'un tel type l'afficheur précise que la variable de type
représentant ce type ouvert est affaiblie. La notation est alors _#pair.
# let
g
=
jolitype
3
;;
val g : ((int, int) _#pair as 'a) -> 'a = <fun>
Si maintenant on applique la fonction g à une paire, on
modifie son type faible.
# g
(new
acc_pair
2
3
);;
- : (int, int) acc_pair = <obj>
# g;;
- : (int, int) acc_pair -> (int, int) acc_pair = <fun>
On ne peut plus alors utiliser g sur des paires simples.
# g
(new
pair
1
1
);;
Characters 4-16:
This expression has type (int, int) pair = < fst : int; snd : int >
but is here used with type
(int, int) acc_pair =
< fst : int; get1 : int -> int; get2 : int -> int; snd : int >
Only the second object type has a method get1
Enfin comme les paramètres de la classe paramétrée peuvent eux aussi s'affaiblir,
on obtient l'exemple suivant.
# let
h
=
jolitype
[];;
val h : (('_b list, '_b list) _#pair as 'a) -> 'a = <fun>
# let
h2
=
h
(new
pair
[]
[
1
;2
]
);;
val h2 : (int list, int list) pair = <obj>
# h;;
- : (int list, int list) pair -> (int list, int list) pair = <fun>
# h
(new
acc_pair
[]
[
4
;5
]
);;
Characters 3-24:
This expression has type
('a list, int list) acc_pair =
< fst : 'a list; get1 : 'a list -> int list; get2 : int list -> 'a list;
snd : int list >
but is here used with type
(int list, int list) pair = < fst : int list; snd : int list >
Only the first object type has a method get1
Remarque
Les classes paramétrées d'Objective CAML sont absolument nécessaires dès que l'on
manipule des méthodes dont le type comporte une variable de type
autre que le type de <<self>>. |