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 :
-
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.
- 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
=
1
0
0
0
*
x1+
1
0
0
*
x2+
1
0
*
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]