Précédent Index Suivant

Extensions du langage

O'Labl apporte trois extensions à Objective CAML. Deux d'entre elles ne modifient pas le reste du langage dans le sens où un programme écrit dans la version 2.04 conserve le même comportement dans la version 2.99. La troisième extension et les modifications qui ont été apportées aux bibliothèques standards sont susceptibles de changer le sens des programmes écrits sans tenir compte des nouveaux traits du langage. Pour assurer une compatibilité avec le passé, il existe deux modes distincts pour utiliser Objective CAML 2.99 :
  1. Le mode classique : c'est l'utilisation par défaut du compilateur et de la boucle d'interaction. L'extension constituée par les labels est restreinte pour ne pas modifier la sémantique des programmes qui ne les utilisent pas.
  2. Le mode moderne : il est choisi explicitement. L'extension des labels permet plus de choses, mais l'utilisation des bibliothèques demande d'utiliser les spécificités de ce nouveau trait.

Moderne ou Classique

Le compilateur et la boucle d'interaction possèdent une nouvelle option (-modern) pour explicitement utiliser les nouveaux traits des labels. Par défaut, le style classique restreint les possibilités des labels et conserve la compatibilité avec les compilateurs précédents.
$ ocamlc -modern .....
$ ocaml -modern .....
La boucle d'interaction autorise de commuter les deux modes pendant son utilisation en utilisant la directive #modern.

# #modern true ;; (* passe en mode moderne *)
# #modern false ;; (* passe en mode classique *)


Warning


Un programme correct en 2.04, même s'il n'utilise aucune des extensions de la 2.99, peut ne plus être correct dans la nouvelle version.


Labels

Un est une annotation apportée aux arguments d'une fonction dans sa déclaration et dans son application. Il se présente comme un identificateur séparé du paramètre (formel ou effectif) d'une fonction par le symbole ':'.

On trouve les labels dans les déclarations de fonctions :

Syntaxe


let fonction label:p = exp
dans les déclarations anonymes avec le mot clé fun :

Syntaxe


fun label:p -> exp
et dans le paramètre effectif d'une fonction :

Syntaxe


( fonction label:exp )


labels dans les types
Les labels donnés aux arguments d'une expression fonctionnelle apparaissent dans son type et annotent les types des arguments auxquels ils réfèrent.

# let add op1:x op2:y = x + y ;;
val add : op1:int -> op2:int -> int = <fun>

# let mk_triplet arg1:x arg2:y arg3:z = (x,y,z) ;;
val mk_triplet : arg1:'a -> arg2:'b -> arg3:'c -> 'a * 'b * 'c = <fun>


Si on souhaite donner le même identificateur au label et à la variable, il n'est pas utile de le répéter car c'est celui pris par défaut.

Syntaxe


fun :p -> exp

# let mk_triplet :arg1 :arg2 :arg3 = (arg1,arg2,arg3) ;;
val mk_triplet : arg1:'a -> arg2:'b -> arg3:'c -> 'a * 'b * 'c = <fun>


L'utilisation du caractère : dans les déclarations de labels impose d'utiliser des espaces pour les autres utilisations de ce symbole à des endroits où il pourrait y avoir ambiguïté.

# let id x:int = int ;; (* x est le label du paramètre int *)
val id : x:'a -> 'a = <fun>
# let id x : int = x ;; (* x est un paramètre de type int *)
val id : int -> int = <fun>


Warning


La contrainte d'un type dans une expression doit se faire en respectant la syntaxe des déclarations de labels.


Il n'est pas autorisé de définir des labels dans une déclaration de fonction par filtrage; en conséquence le mot clé function ne peut pas être utilisé pour une fonction avec label.

# let f = function arg:x -> x ;;
Characters 18-22:
Syntax error
# let f = fun arg:x -> x ;;
val f : arg:'a -> 'a = <fun>


labels en mode classique

Dans un premier temps nous présentons les labels dans le mode classique.

Application
Les labels peuvent être utilisés pour appliquer les arguments à une fonction, mais ils ne sont pas obligatoires.

# mk_triplet '1' 2 3.0 ;;
- : char * int * float = '1', 2, 3
# mk_triplet arg1:'1' arg2:2 arg3:3.0 ;;
- : char * int * float = '1', 2, 3
# mk_triplet '1' arg2:2 3.0 ;;
- : char * int * float = '1', 2, 3


Les labels ne modifient pas les possibilités d'application partielle.

# let couple = mk_triplet arg1:() ;;
val couple : arg2:'_a -> arg3:'_b -> unit * '_a * '_b = <fun>


Warning


Dans le mode classique, les arguments d'une fonction doivent être appliqués dans l'ordre de leur déclaration.


# let couple = mk_triplet arg2:() ;;
Characters 30-32:
Expecting function has type arg1:'a -> arg2:'b -> arg3:'c -> 'a * 'b * 'c
This argument cannot be applied with label arg2:


Lisibilité du code
L'intérêt principal des labels est d'associer au type d'un argument une annotation qui rend plus explicite l'interface d'une fonction.

# String.sub ;;
- : string -> pos:int -> len:int -> string = <fun>
Plus aucun doute, la fonction String.sub prend en argument une chaîne, la position du premier caractère, et la longueur de la chaîne à extraire.

Les fonctions des bibliothèques de la distribution d'Objective CAML 2.99 ont été << labelisées >>. Le tableau B.1 donne les conventions qui ont été utilisées.


label signification
pos: une position (dans une liste, une chaîne, un tableau, etc.)
len: une taille (length)
buf: une chaîne utilisée comme tampon (buffer)
src: la source d'une opération
dst: la destination d'une opération
fun: une fonction à appliquer
pred: un prédicat
acc: un accumulateur
to: un canal de sortie (out_channel)
key: une clé utilisée dans un index (liste d'associations, etc.)
data: une valeur associée utilisée dans un index
mode: un mode ou une liste d'options
perm: des permissions de fichiers

Figure B.1 : conventions sur les labels


Labels en mode moderne

La principale différence avec le mode classique est que dans le mode moderne, il est obligatoire de donner explicitement les labels lors de l'application d'une expression fonctionnelle.

# let add :x :y = x + y ;;
val add : x:int -> y:int -> int = <fun>
# #modern false ;;
# add 1 2 ;;
- : int = 3
# #modern true ;;
# add 1 2 ;;
Characters 4-5:
Expecting function has type x:int -> y:int -> int
This argument cannot be applied without label


Warning


Dans le mode moderne, il est obligatoire de donner le nom des labels aux arguments qui en possèdent un.


La conséquence de cette obligation est que l'ordre des arguments possédant un label devient pour le coup indifférent puisqu'on peut les identifier par leur label. Objective CAML 2.99 possède un système permettant de commuter les arguments d'une fonction et donc de les appliquer dans n'importe quel ordre.

# add x:1 y:2 ;;
- : int = 3
# add y:2 x:1 ;;
- : int = 3


C'est particulièrement utile quand on souhaite faire l'application partielle d'un argument qui n'est pas le premier de la déclaration.

# let add_x_2 = add y:2 ;;
val add_x_2 : x:int -> int = <fun>
# add_x_2 x:1 ;;
- : int = 3


Les arguments possédant le même label ou n'ayant pas de label ne commutent pas entre eux. Une application dans un tel cas considère le premier argument qui a ce label (respectivement qui n'a pas de label).

# let test arg1:_ arg2:_ _ arg2:_ _ = () ;;
val test : arg1:'a -> arg2:'b -> 'c -> arg2:'d -> 'e -> unit = <fun>

# test arg2:() ;; (* le premier label arg2: de la déclaration *)
- : arg1:'a -> 'b -> arg2:'c -> 'd -> unit = <fun>

# test () ;; (* le premier argument sans label de la déclaration *)
- : arg1:'a -> arg2:'b -> arg2:'c -> 'd -> unit = <fun>


Paramètres optionnels

Objective CAML 2.99 autorise la définition de fonctions avec des arguments labelisés optionnels. De tels arguments sont définis avec une valeur par défaut qui est la valeur donnée au paramètre si l'application n'en donne pas une autre explicitement.

Syntaxe


fun ?(nom:p = exp1) -> exp2


Le caractère optionnel de l'argument apparaît dans son type par le symbole ?.

# let sp_incr ?(inc:x=1) y =  y := !y + x ;;
val sp_incr : ?inc:int -> int ref -> unit = <fun>
La fonction sp_incr se comporte comme la fonction incr du module Pervasives.

# let v = ref 4 in sp_incr v ; v ;;
- : int ref = {contents=5}
Mais on peut lui spécifier un incrément différent de celui par défaut.

# let v = ref 4 in sp_incr inc:3 v ; v ;;
- : int ref = {contents=7}


L'application d'une fonction se fait en donnant la valeur par défaut de tous les paramètres optionnels jusqu'au paramètre effectivement passé par l'application. Si l'argument de l'appel est donné sans label, il est considéré comme étant le premier argument de la fonction qui ne soit pas optionnel.

# let f ?(:x1=0) ?(:x2=0) x3 x4 = 1000*x1+100*x2+10*x3+x4 ;;
val f : ?x1:int -> ?x2:int -> int -> int -> int = <fun>
# f 3 ;;
- : int -> int = <fun>
# f 3 4 ;;
- : int = 34
# f x1:1 3 4 ;;
- : int = 1034
# f x2:2 3 4 ;;
- : int = 234


Les arguments optionnels peuvent être passés dans un ordre arbitraire quel que soit le mode en cours.

# #modern false ;;
# f x2:2 x1:1 3 4 ;;
- : int = 1234


Un argument optionnel peut être donné sans valeur par défaut, dans ce cas il est considéré dans le corps de la fonction comme étant du type 'a option; None est sa valeur par défaut.

Syntaxe


fun ?nom:p -> exp


# let print_entier ?file:x n = 
match x with
None -> print_int n
| Some f -> let fic = open_out f in
output_string fic (string_of_int n) ;
output_string fic "\n" ;
close_out fic ;;
val print_entier : ?file:string -> int -> unit = <fun>
Par défaut, la fonction print_entier affiche son argument sur la sortie standard. Si on lui passe un nom de fichier avec le label file, la sortie se fait alors dans ce fichier.

Remarque


Si les derniers paramètres d'une fonction sont optionnels, il devront être appliqués explicitement.

# let test ?:x ?:y n ?:a ?:b = n ;;
val test : ?x:'a -> ?y:'b -> 'c -> ?a:'d -> ?b:'e -> 'c = <fun>
# test 1 ;;
- : ?a:'_a -> ?b:'_b -> int = <fun>
# test 1 b:'x' ;;
- : ?a:'_a -> int = <fun>
# test 1 a:() b:'x' ;;
- : int = 1


Labels et objets

Les labels peuvent être utilisés pour les paramètres d'une méthode ou pour ceux du constructeur d'un objet.

# class point ?(:x=0) ?(:y=0) (col : Graphics.color) = 
object
val pos = (x,y)
val couleur = col
method affiche ?(to:file=stdout) () =
output_string file "point (" ;
output_string file (string_of_int (fst pos)) ;
output_string file "," ;
output_string file (string_of_int (snd pos)) ;
output_string file ")\n"
end ;;
class point :
?x:int ->
?y:int ->
Graphics.color ->
object
val couleur : Graphics.color
val pos : int * int
method affiche : ?to:out_channel -> unit -> unit
end

# let obj1 = new point x:1 y:2 Graphics.white
in obj1#affiche () ;;
point (1,2)
- : unit = ()
# let obj2 = new point Graphics.black
in obj2#affiche () ;;
point (0,0)
- : unit = ()


Les labels et les arguments optionnels fournissent une alternative à la surcharge des méthodes et des constructeurs qui sont classiques dans les langages à objets et qu'Objective CAML ne peut accepter.

Cette émulation de la surcharge comporte quelques limitations, en particulier un des arguments devra ne pas être optionnel; on peut toujours utiliser un argument de type unit.

# class nombre ?:entier ?:flottant () = 
object
val mutable valeur = 0.0
method print = print_float valeur
initializer
match (entier,flottant) with
(None,None) | (Some _,Some _) -> failwith "construction incorrecte"
| (None,Some f) -> valeur <- f
| (Some n,None) -> valeur <- float_of_int n
end ;;
class nombre :
?entier:int ->
?flottant:float ->
unit -> object val mutable valeur : float method print : unit end

# let n1 = new nombre entier:1 () ;;
val n1 : nombre = <obj>
# let n2 = new nombre flottant:1.0 () ;;
val n2 : nombre = <obj>


Constructeurs polymorphes

Les types sommes d'Objective CAML ont deux principales limitations. D'une part il n'est pas possible d'étendre un type somme avec un nouveau constructeur. D'autre part un constructeur ne peut appartenir qu'à un seul type. Objective CAML 2.99 propose des constructeurs spéciaux dit constructeurs polymorphes qui échappent à ces deux contraintes.

Un constructeur polymorphe est bâti avec un identificateur qui a la même contrainte syntaxique que les autres constructeurs à savoir qu'il doit être préfixé par une majuscule.

Syntaxe


`Nom
ou

Syntaxe


`Nom type


Un ensemble de constructeurs polymorphes forme un type, mais il n'est pas nécessaire de définir un type pour définir un constructeur polymorphe.

# let x =  `Entier 3 ;;
val x : [>`Entier int] = `Entier 3


Le type de x avec le symbole [> se lit comme le type qui contient au moins le constructeur `Entier int.

# let int_of = function 
`Entier n -> n
| `Reel r -> int_of_float r ;;
val int_of : [<`Entier int|`Reel float] -> int = <fun>


A l'inverse, le symbole [< indique que l'argument de int_of est du type qui contient au plus les constructeurs `Entier int et `Reel float.

Il est tout de même possible de définir un type par énumération de ses constructeurs polymorphes :

Syntaxe


type t = [ `Nom1 | `Nom2 | ... | `Nomn ]
ou pour des types paramétrés :

Syntaxe


type ('a,'b,...) t = [ `Nom1 | `Nom2 | ... | `Nomn ]


# type valeur = [ `Entier int | `Reel float ] ;;
type valeur = [`Entier int|`Reel float]


Les constructeurs polymorphes, comme leur nom l'indique, peuvent prendre des arguments de plusieurs valeurs.

# let v1 = `Nombre 2
and v2 = `Nombre 2.0 ;;
val v1 : [>`Nombre int] = `Nombre 2
val v2 : [>`Nombre float] = `Nombre 2
Mais pour autant, v1 et v2 ne sont pas du même type.

# v1=v2 ;;
Characters 4-6:
This expression has type [>`Nombre float] but is here used with type
[>`Nombre int]


D'une manière plus générale, les contraintes sur le type des arguments des constructeurs polymorphes sont accumulées dans leur type par l'annotation &.

# let test_nul_entier = function `Nombre n -> n=0 
and test_nul_reel = function `Nombre r -> r=0.0 ;;
val test_nul_entier : [<`Nombre int] -> bool = <fun>
val test_nul_reel : [<`Nombre float] -> bool = <fun>
# let test_nul x = (test_nul_entier x) || (test_nul_reel x) ;;
val test_nul : [<`Nombre int & float] -> bool = <fun>
Le type de test_nul indique que les seules valeurs acceptées par cette fonction sont celles avec le constructeur `Nombre et un argument qui est à la fois de type int et de float. C'est à dire que les seules valeurs acceptables sont de type 'a !

# let f () = test_nul (failwith "rend une valeur de type 'a") ;;
val f : unit -> bool = <fun>


Les types des constructeurs polymorphes sont eux-mêmes susceptibles d'être polymorphes.

# let id = function `Ctor -> `Ctor ;;
val id : [<`Ctor] -> [>`Ctor] = <fun>
Le type de la valeur de retour de id est << les ensembles de constructeurs qui contiennent au moins `Ctor >> donc c'est un type polymorphe qui peut s'instancier en un type plus précis. De même, l'argument de id est << les ensembles de constructeurs qui contiennent au plus `Ctor >> qui lui aussi est susceptible d'être précisé. En conséquence, ils suivent le mécanisme général des types polymorphes d'Objective CAML à savoir qu'ils sont susceptibles d'être affaiblis.

# let v = id `Ctor ;;
val v : _[>`Ctor] = `Ctor
v étant le résultat d'une application, son type n'est pas polymorphe (d'où la présence du caractère _).

# id v ;;
- : _[>`Ctor] = `Ctor
v est monomorphe et son type est un sous-type de << contient au moins le constructeur `Ctor >>. Le fait de l'appliquer à id va forcer son type à être un sous-type de << contient au plus le constructeur `Ctor >>. Logiquement, il doit maintenant avoir le type << contient exactement `Ctor >>. Ce que nous vérifions.

# v ;;
- : [`Ctor] = `Ctor


À la manière des classes, les types des constructeurs polymorphes peuvent être ouverts.

# let is_entier = function
`Entier (n : int) -> true
| _ -> false ;;
val is_entier : [<`Entier int| ..] -> bool = <fun>
# is_entier (`Entier 3) ;;
- : bool = true
# is_entier `Autre ;;
- : bool = false
Tous les constructeurs sont acceptés, mais le constructeur `Entier doit avoir un argument entier.

# is_entier (`Entier 3.0) ;;
Characters 12-23:
This expression has type [>`Entier float] but is here used with type
[<`Entier int| ..]


Toujours comme les classes, les types des constructeurs peuvent être cycliques.

# let rec long = function `Rec x -> 1 + (long x) ;;
val long : ([<`Rec 'a] as 'a) -> int = <fun>


Pour finir, notons que le type peut être en même temps un sous-ensemble et un sur ensemble de constructeurs. Commençons par un exemple simple.

# let ex1 = function `C1 -> `C2 ;;
val ex1 : [<`C1] -> [>`C2] = <fun>
Maintenant nous identifions les types d'entrée et de sortie de l'exemple par un second filtre.

# let ex2 = function `C1 -> `C2 | x -> x ;;
val ex2 : ([<`C2|`C1| .. >`C2] as 'a) -> 'a = <fun>
Nous obtenons donc le type ouvert qui contient au moins `C2 puisque le type de retour contient au moins `C2.

# ex2 ( `C1 : [> `C1 ] ) ;; (* est bien un sous type de [<`C2|`C1| .. >`C2] *)
- : _[>`C2|`C1] = `C2
# ex2 ( `C1 : [ `C1 ] ) ;; (* n'est pas un sous type de [<`C2|`C1| .. >`C2] *)
Characters 6-9:
This expression has type [`C1] but is here used with type [<`C2|`C1| .. >`C2]



Précédent Index Suivant