Un label est un nom que l'on donne à un paramètre d'une fonction.
Par exemple, je voudrais écrire un fonction valid_date : int -> int -> bool
qui me dit si une date est valide (en supposant l'année bisextile).
Les deux arguments entiers sont le jour du mois et le numéro du mois.
Si je veux savoir si le 4 juillet est une date valide, j'écrirai sans doute valid_date 4 7
.
Mon oncle d'Amérique, lui, écrira probablement valid_date 7 4
.
On peut documenter la fonction pour éviter les erreurs, certes, mais avec les labels, on peut faire mieux!
let valid_date ~day ~month = (* <- notez bien le ~ devant day et month ! *)
day > 0 &&
month > 0 &&
month < 13 &&
day < [|0;31;28;31;30;31;30;31;31;30;31;30;31|].(month)
val valid_date : day:int -> month:int -> bool = <fun>
Notez bien que le type de valid_date
est maintenant day:int -> month:int -> bool
.
Pour utiliser ma fonction, je dois rappeler les labels devant les arguments
let _ = valid_date ~day:4 ~month:7
- : bool = true
Comme ils sont nommés, je peux maintenant donner les arguments dans l'ordre que je veux, ou encore fixer un des deux arguments, au choix, dans une application partielle.
let _ = valid_date ~month:7 ~day:4
- : bool = true
let check_day = valid_date ~month:1
val check_day : day:int -> bool = <fun>
let check_month = valid_date ~day:10
val check_month : month:int -> bool = <fun>
Lorsque le nom du label (disons bla
) est le même que le nom de l'argument (s'il s'agit d'une variable), on peut écrire simplement ~bla
au lieu de ~bla:bla
let day = 13
let _ = valid_date ~day ~month:12
val day : int = 13
- : bool = true
On peut aussi écrire des fonctions d'ordre supérieur qui prennent en argument des fonctions avec des labels
let instanciate_day f day = f ~day
let foo = instanciate_day valid_date 4
let check_month month = instanciate_day valid_date 4 ~month
val instanciate_day : (day:'a -> 'b) -> 'a -> 'b = <fun>
val foo : month:int -> bool = <fun>
val check_month : int -> bool = <fun>
On peut fixer une valeur par défaut à un argument labélisé, et de ce fait le rendre optionnel.
?(nom_arg=valeur_defaut)
~nom_arg:autre_valeur
let string_of_date ?(year=2019) day month =
Format.sprintf "%02d/%02d/%d" day month year
let _ = string_of_date 14 7
let _ = string_of_date ~year:1789 14 7
val string_of_date : ?year:int -> int -> int -> string = <fun>
- : string = "14/07/2019"
- : string = "14/07/1789"
Il est recommandé de faire suivre un argument optionnel par un argument non optionnel. Cela permet d'identifier lors de l'appel de fonction si l'argument optionnel a bien été passé implicitement avec sa valeur par défaut
let string_of_date_bad day month ?(year=2019) = ""
File "[85]", line 1, characters 35-44: 1 | let string_of_date_bad day month ?(year=2019) = "" ^^^^^^^^^ Warning 16: this optional argument cannot be erased.
val string_of_date_bad : 'a -> 'b -> ?year:int -> string = <fun>
On peut aussi ne pas définir de valeur par défaut pour un argument optionnel.
Dans ce cas-là, il est traité comme un argument de type 'a option
en interne.
let string_of_date ?year day month =
match year with
| None -> Format.sprintf "%02d/%02d" day month
| Some y -> Format.sprintf "%02d/%02d/%d" day month y
let _ = string_of_date 14 7
let _ = string_of_date ~year:1789 14 7
val string_of_date : ?year:int -> int -> int -> string = <fun>
- : string = "14/07"
- : string = "14/07/1789"
Les labels sont une alternative intéressante au polymorphisme par surcharge courant dans les langages objets.
En Java, je peux définir une méthode valid_date(int day, int month)
et une autre méthode valid_date(int day, int month, int year)
qui ont le même nom mais qui n'ont pas les mêmes types d'arguments, ni même nécessairement la même valeur de retour.
OCaml est plus restrictif avec les labels, mais il permet dans de nombreux cas de reproduire ce qu'on ferait en Java.
Un objet regroupe dans une même entité des champs qui contiennent des valeurs et des méthodes qui permettent de les manipuler.
Pour fixer les idées, nous allons considérer un objet compteur
qui dispose de :
n:int
qui contient la valeur du compteurget_valeur:int
qui renvoie la valeur du compteurincr: int -> unit
qui incrémente le compteur d'une valeur donnéelet compteur0 = object
val mutable n = 0
method get_valeur = n
method incr k = n <- n + k
end
val compteur0 : < get_valeur : int; incr : int -> unit > = <obj>
Le type d'un objet indique quelles méthodes peuvent être appellées pour cet objet.
Pour appeler la méthode m
d'un objet o
, on écrit o#m
.
compteur0#get_valeur
- : int = 0
let () = compteur0#incr 1
compteur0#get_valeur
- : int = 1
Il n'est pas possible de lire directement la valeur d'un champs à l'extérieur de l'objet sans une méthode "accesseur" comme get_valeur
.
compteur0#n
File "[15]", line 1, characters 0-9: 1 | compteur0#n ^^^^^^^^^ Error: This expression has type < get_valeur : int; incr : int -> unit > It has no method n
Il n'est pas possible de modifier un champs dans une méthode s'il n'a pas été déclaré mutable
let compteur0 = object
val (* mutable *) n = 0
method get_valeur = n
method incr k = n <- n +k
end
File "[16]", line 4, characters 18-27: 4 | method incr k = n <- n +k ^^^^^^^^^ Error: The instance variable n is not mutable
La forme let ... = object ... end
permet de créer un objet immédiat. Cependant, en général, on préfère voir les objets comme des instances d'une classe...
Une classe est une "fabrique" à objets. À partir d'une classe compteur
, je vais pouvoir créer plusieurs objets: ils auront en commun les méthodes get_valeur
et incr
, mais chaque objet aura son propre champs n
.
class compteur = object
val mutable n = 0
method get_valeur = n
method incr k = n <- n + k
end
class compteur : object val mutable n : int method get_valeur : int method incr : int -> unit end
Pour créer un objet o
instance de de la classe c
, j'écris let o = new c
.
let compteur1 = new compteur
let compteur2 = new compteur
val compteur1 : compteur = <obj>
val compteur2 : compteur = <obj>
compteur1#incr 1
- : unit = ()
compteur1#get_valeur
- : int = 1
compteur2#get_valeur
- : int = 0
La classe peut prendre des arguments afin d'initialiser correctement les champs de l'objet. Par exemple, la classe compteur peut prendre en argument la valeur initiale du compteur.
class compteur n_init = object
val mutable n = n_init
method get_valeur = n
method incr k = n <- n + k
end
class compteur : int -> object val mutable n : int method get_valeur : int method incr : int -> unit end
let compteur3 = new compteur 42
val compteur3 : compteur = <obj>
compteur3#get_valeur
- : int = 42
Une méthode d'un objet peut en appeler une autre sur le même objet.
Cet objet n'est pas connu au moment où on écrit la méthode: un nom spécial (en général self
) est utilisé pour en parler.
class compteur n_init = object (self)
val mutable n = n_init
method get_valeur = n
method incr k =
n <- self#get_valeur + k (* <- self# est nécessaire *)
end
class compteur : int -> object val mutable n : int method get_valeur : int method incr : int -> unit end
Note : le nom self
n'est pas un mot-clé, on choisit comment s'appeler soi-même juste après le mot-clé object
. Parfois on préfère s'appeler this
.
class fibo_naif = object (this)
method fibo n =
if n < 2
then 1
else this#fibo (n-1) + this#fibo (n-2)
end
class fibo_naif : object method fibo : int -> int end
On peut définir une classe c
en étendant une autre classe d
.
On dit que c
hérite de d
.
class compteur_identifie n_init (id_init:string) = object
inherit compteur n_init
val id = id_init
method affiche = Format.sprintf "%s vaut %d" id n
end
class compteur_identifie : int -> string -> object val id : string val mutable n : int method affiche : string method get_valeur : int method incr : int -> unit end
(* definition alternative avec self *)
class compteur_identifie n_init (id_init:string) = object (self)
inherit compteur n_init
val id = id_init
method affiche = Format.sprintf "%s vaut %d" id self#get_valeur
end
class compteur_identifie : int -> string -> object val id : string val mutable n : int method affiche : string method get_valeur : int method incr : int -> unit end
let compteur4 = new compteur_identifie 42 "toto"
val compteur4 : compteur_identifie = <obj>
compteur4#affiche
- : string = "toto vaut 42"
En cas d'héritage, je peux redéfinir une méthode m
plutôt que d'en hériter sans la changer.
Ceci peut avoir un effet de bord: changer m
peut aussi changer le comportement d'une méthode qui appelle la méthode m
.
class chenille = object (self)
method nom = "chenille"
method affiche = self#nom
end
class papillon = object
inherit chenille
method! nom = "papillon" (* <- redéfinition *)
end
class chenille : object method affiche : string method nom : string end
class papillon : object method affiche : string method nom : string end
Note: on peut utiliser method!
au lieu de method
en cas de redéfinition, mais ce n'est pas obligé (c'est le @Override
de Java).
let papillon0 = new papillon
val papillon0 : papillon = <obj>
papillon0#nom
- : string = "papillon"
papillon0#affiche
- : string = "papillon"
Il faut bien comprendre ce qu'il se passe:
papillon0#nom
, c'est la méthode redéfinie qui est appeléepapillon0#affiche
, c'est la méthode affiche
de la classe chenille
qui est appelée, mais l'appel self#nom
que contient affiche
est évalué en remplaçant self
par papillon0
: c'est donc bien papillon0#nom
qui est appelé.On parle de liaison tardive ou de liaison dynamique.
Et la liaison statique? C'est la liaison "habituelle": quand une fonction en appele une autre, on sait à l'avance quelle est cette autre fonction, il ne sera pas possible de la remplacer par une autre.
let nom () = "chenille"
let affiche () = Format.sprintf "%s vaut 0" (nom ())
let nom () = "papillon"
val nom : unit -> string = <fun>
val affiche : unit -> string = <fun>
val nom : unit -> string = <fun>
let _ = affiche ()
- : string = "chenille vaut 0"
Lorsqu'on redéfinit une méthode, il est parfois utile de pouvoir appeler la version antérieure de la méthode. Il suffit de donner un nom (en général super
) à la classe dont on hérite
class papillon = object (self)
inherit chenille as super
method nom =
Format.sprintf "papillon (anciennement %s)" super#nom
end
class papillon : object method affiche : string method nom : string end
(new papillon)#nom
- : string = "papillon (anciennement chenille)"
Revenons sur la classe fibo
de tout à l'heure
class fibo_naif = object (self)
method fibo n =
if n < 2
then 1
else self#fibo(n-1) + self#fibo(n-2)
end
class fibo_naif : object method fibo : int -> int end
let fibo0 = new fibo_naif
val fibo0 : fibo_naif = <obj>
List.init 10 (fun n -> fibo0#fibo n)
- : int list = [1; 1; 2; 3; 5; 8; 13; 21; 34; 55]
fibo0#fibo 50
Ce programme ne termine pas (dans un temps raisonable). Vous vous souvenez pourquoi?
Une solution pour éviter de répéter des calculs déjà effectués consiste à les mémoriser à l'aide d'une table de hachage.
Nous allons mémoïser la méthode/fonction fibo
de deux façons différentes:
fibo
déjà calculées. Au moment de créer la table de hachage, OCaml
a besoin d'une estimation de la taille de ma table de hachage. Pour nous, ce sera 50.let memoire = Hashtbl.create 50
val memoire : ('_weak3, '_weak4) Hashtbl.t = <abstr>
fibo
let rec fibo n =
try Hashtbl.find memoire n (* si j'ai deja calcule fibo n *)
with Not_found ->
let res = (* sinon, je le calcule *)
if n < 2
then 1
else fibo(n-1) + fibo(n-2)
in
Hashtbl.add memoire n res; (* je memorise fibo n *)
res (* je renvoie le résultat *)
val fibo : int -> int = <fun>
fibo 50
- : int = 20365011074
Maintenant, je vais définir une classe fibo_memo
qui contient une méthode fibo
mémoïsée en deux étapes:
fibo_naif
qui contient une méthode fibo
non mémoïséefibo_memo
qui hérite de fibo_naif
et qui redéfinit la méthode fibo
de la façon suivante:fibo n
est en mémoire, on renvoie le résultat directementfibo
de la classe fibo_naif
. class fibo_naif = object (self)
method fibo n =
if n < 2
then 1
else self#fibo (n-1) + self#fibo (n-2)
end
class fibo_memo =
object (self)
inherit fibo_naif as super
val memoire = Hashtbl.create 50
method fibo n =
try Hashtbl.find memoire n
with Not_found ->
let res = super#fibo n in
Hashtbl.add memoire n res;
res
end
class fibo_naif : object method fibo : int -> int end
class fibo_memo : object val memoire : (int, int) Hashtbl.t method fibo : int -> int end
let fibo1 = new fibo_memo
let _ = fibo1#fibo 50
val fibo1 : fibo_memo = <obj>
- : int = 20365011074
C'est l'un des aspects sur lesquels les langages objets mainstream ont des positions différentes: Java n'autorise pas l'héritage multiple, mais C++ si.
En OCaml, l'héritage multiple est autorisé, mais l'ordre dans lequel on liste les classes dont on hérite joue un rôle.
class a = object method ma = "a" end
class b = object method mb = "b" end
class ab = object inherit a inherit b end
class a : object method ma : string end
class b : object method mb : string end
class ab : object method ma : string method mb : string end
let o = new ab in (o#ma ^ o#mb)
- : string = "ab"
class a = object method m = "a" end
class b = object inherit a method! m = "b" end
class c = object inherit a method! m = "c" end
class d = object (self) inherit b inherit c end
class a : object method m : string end
class b : object method m : string end
class c : object method m : string end
class d : object method m : string end
(new d)#m
- : string = "c"
Il est parfois utile de faire une copie d'un objet avant de le modifier, pour ne pas perdre l'état dans lequel il était. La façon la plus simple est d'utiliser la fonction Oo.copy
.
class compteur = object
val mutable n = 0
method inc = n <- n + 1
method get = n
end
class compteur : object val mutable n : int method get : int method inc : unit end
let c1 = new compteur
let () = c1#inc
let c2 = Oo.copy c1
let () = c1#inc
val c1 : compteur = <obj>
val c2 : compteur = <obj>
c2#get
- : int = 1
{<
...>}
¶À l'intérieur d'une méthode, on peut utiliser la syntaxe {<
...>}
pour créer une copy de self
. Cette syntaxe permet de redéfinir des champs ou des méthodes dans la copie.
class compteur = object (self)
val mutable n = 0
method get = n
method increment = n <- n + 1
method copy = {< >} (*equivaut à Oo.copy self *)
method incremented = {< n = self#get + 1 >}
end
class compteur : object ('a) val mutable n : int method copy : 'a method get : int method increment : unit method incremented : 'a end
Rappel la notation {< n = ... >}
permettant de redéfinir la valeur du champs n
de l'objet dans la copie est proche de la notation { r with n = ...}
que nous avons vue dans le cours sur les enregistrements.
Vous vous souvenez de ce que fait {r with n=...}
?
let c1 = new compteur
let () = c1#increment
let c2 = c1#copy
let () = c1#increment
val c1 : compteur = <obj>
val c2 : compteur = <obj>
c2#get, c1#get
- : int * int = (1, 2)
let c3 = c1#incremented
val c3 : compteur = <obj>
c3#get, c1#get
- : int * int = (3, 2)
new
¶Il y a une différence assez subtile entre cloner un objet et en allouer un nouveau en recopiant les champs.
class compteur n_init = object (self)
val mutable n = n_init
method get = n
method inc = n <- n + 1
method copy = new compteur self#get (* differe de Oo.copy self *)
end
class compteur : int -> object val mutable n : int method copy : compteur method get : int method inc : unit end
Cette différence se voit lorsqu'on étend la classe:
new
spécifie la classe (ici compteur
): on risque de "sous-classer" l'objet que l'on clonePrécisons sur un exemple.
class entier (n_init:int) = object (self)
val mutable n = n_init
method get = n
method copy = new entier self#get (* <- entier !*)
end
class compteur n = object
inherit entier n
method inc = n <- n + 1
end
class entier : int -> object val mutable n : int method copy : entier method get : int end
class compteur : int -> object val mutable n : int method copy : entier method get : int method inc : unit end
let c1 = new compteur 1
let c2 = c1#copy
val c1 : compteur = <obj>
val c2 : entier = <obj>
c2#inc
File "[62]", line 1, characters 0-2: 1 | c2#inc ^^ Error: This expression has type entier It has no method inc
Je veux définir une classe compteur comme celle vue jusqu'à présent, et je souhaite créer plusieurs compteurs par la suite.
Je voudrais cependant ajouter la possibilité de remettre à 0 tous les compteurs que j'ai déjà créés en appelant une méthode reset_all
.
Comment faire?
Analysons le problème
Il va me falloir maintenir une liste du peuple
des compteurs. Cette liste ne peut pas être un champs de compteur, elle doit être commune à tous les compteurs: c'est une variable de classe. Il faudra la mettre à jour à chaque création de compteur.
La méthode reset_all
ne pourra modifier directement que la valeur du compteur sur lequel elle est appellée. Il faudra donc ajouter une méthode reset
pour remettre à 0 un compteur, et appeler cette méthode sur chaque compteur de la liste depuis reset_all
.
Le typage des objets est délicat en OCaml. On ne fera qu'effleurer le sujet...
La première chose à comprendre, c'est que deux classes différentes définissent deux types différents incompatibles, même si l'une hérite de l'autre.
Ceci a une conséquence directe sur la possibilité de ranger dans une même liste des objets de classes différentes.
class class1 = object method m1 = "m1" end
class class2 = object inherit class1 method m2 ="m2" end
let c1 = new class1
let c2 = new class2
class class1 : object method m1 : string end
class class2 : object method m1 : string method m2 : string end
val c1 : class1 = <obj>
val c2 : class2 = <obj>
c1::c2::[]
File "[80]", line 1, characters 4-6: 1 | c1::c2::[] ^^ Error: This expression has type class2 but an expression was expected of type class1 The second object type has no method m2
La solution consiste à "sous-classer" l'objet c2
pour pouvoir le mettre dans la même liste que c1
. Pour cela, on utilise une coercion de la forme (e :> sous_classe)
(ou parfois (e:classe:>sous_classe)
)
let c2_1 = (c2:>class1)
val c2_1 : class1 = <obj>
c2_1#m1
- : string = "m1"
c2_1#m2
File "[83]", line 1, characters 0-4: 1 | c2_1#m2 ^^^^ Error: This expression has type class1 It has no method m2
La solution pour mettre c1
et c2
ensemble dans une même liste consiste donc à sous-classer c2
à l'aide d'une coercion.
class class1 = object method m = "m de c1" end
class class2 = object inherit class1 method! m = "m de c2" method n = "n" end
let c1, c2 = new class1, new class2
class class1 : object method m : string end
class class2 : object method m : string method n : string end
val c1 : class1 = <obj> val c2 : class2 = <obj>
let l = c1 :: (c2 :> class1) :: []
val l : class1 list = [<obj>; <obj>]
List.map (fun c-> c#m) l
- : string list = ["m de c1"; "m de c2"]
Remarque une coercion s'apparente à un cast en C. Mais contrairement au cast, une coercion ne casse pas le système de typage.
Revenons à notre problème: un peuple de compteurs réinitialisables.
module Compteur_Non_Signe = struct
class type resetable = object method reset : unit end
let peuple : resetable list ref = ref []
let naissance c = peuple := (c :> resetable) :: !peuple
class compteur = object (self)
initializer naissance self (* <- initialiseur *)
val mutable n = 0
method get = n
method inc = n <- n + 1
method reset = n <- 0
method reset_all = List.iter (fun c-> c#reset) !peuple
method copy = let res = {<>} in naissance res; res
end
end
module Compteur_Non_Signe : sig class type resetable = object method reset : unit end val peuple : resetable list ref val naissance : #resetable -> unit class compteur : object ('a) val mutable n : int method copy : 'a method get : int method inc : unit method reset : unit method reset_all : unit end end
Remarque L'initialiseur est appelé à chaque création d'objet par new
. Il n'est pas appelé par clonage, c'est pourquoi on privilégie une méthode copy
qui répète l'initialiseur.
Pour finir, je peux signer mon module afin de rendre privé l'interface (class type) resetable
et la variable peuple
.
module type COMPTEUR = sig
class compteur : object ('a)
method get : int
method inc : unit
method reset : unit
method reset_all : unit
method copy : 'a
end
end
module type COMPTEUR = sig class compteur : object ('a) method copy : 'a method get : int method inc : unit method reset : unit method reset_all : unit end end
module Compteur : COMPTEUR = Compteur_Non_Signe
module Compteur : COMPTEUR
let c1,c2 = Compteur.(new compteur, new compteur)
let c3 = c1#copy
let () = c1#inc; c2#inc; c2#inc; c3#inc;c3#inc; c3#inc
let _ = c1#get, c2#get, c3#get
val c1 : Compteur.compteur = <obj> val c2 : Compteur.compteur = <obj>
val c3 : Compteur.compteur = <obj>
- : int * int * int = (1, 2, 3)
let () = c1#reset_all
let _ = c1#get, c2#get, c3#get
- : int * int * int = (0, 0, 0)
Compteur.peuple
File "[111]", line 1, characters 0-15: 1 | Compteur.peuple ^^^^^^^^^^^^^^^ Error: Unbound value Compteur.peuple
Nous avons vu beaucoup de choses en une seule séance, et pourtant nous n'avons fait qu'effleurer le sujet... Résumons.
method!
)m
héritée sans la redéfinir, mais en redéfinissant d'autres méthodes que la méthode m
appelleOo.copy
ou {<>}
n'est pas la même chose qu'en allouer un autre avec new
(o :> sous_classe)
Rendez-vous au cours de POO de L3 pour revoir ces notions et pour en découvrir bien d'autres (classes virtuelles, méthodes privées, etc)