Types de base
Comment faire des calculs en précision arbitraire ?
OCaml et Caml Light disposent tous deux d'une bibliothèque de calcul en précision arbitraire
précision, sur les nombres rationnels. La bibliothèque se nomme
Les opérations sur les grands nombres sont suffixées par le caractère
Num
en OCaml et camlnum
en Caml Light.
Les opérations sur les grands nombres sont suffixées par le caractère
/
: par exemple l'addition se note
+/
. On crée des grands nombres par conversion à
partir d'entiers ou de chaînes de caractères. Pour l'affichage dans
le toplevel, il peut être utile d'utiliser un imprimeur. Un exemple
d'utilisation pour OCaml est donné plus bas.
$ ocaml nums.cma open Num;; open Format;; let print_num ff n = fprintf ff "%s" (string_of_num n);; print_num : Format.formatter -> num -> unit = <fun>; #install_printer print_num;; num_of_string "2/3";; - : Num.num = 2/3 let n = num_of_string "1/3" +/ num_of_string "2/3";; n : Num.num = 1 let rec fact n = if n <= 0 then (num_of_int 1) else num_of_int n */ (fact (n - 1));; fact : int -> Num.num = <fun>; fact 100;; - : Num.num = 9332621544394415268169923885626670049071596826438162146859296389521759999322991 5608941463976156518286253697920827223758251185210916864000000000000000000000000
Structures de données
Mon tableau est modifié sans raison
En général cela provient d'un partage physique passé
inaperçu. En Caml, il n'y a jamais de recopie implicite de
tableaux. Si donc l'on donne deux noms à un tableau, toute
modification citant l'un des noms se répercute sur l'autre:
let v = Array.make 3 0;; val v : int array = [|0; 0; 0|] let w = v;; val w : int array = [|0; 0; 0|] w.(0) <- 4;; - : unit = () v;; - : int array = [|4; 0; 0|]Le partage physique apparaît également au niveau des éléments stockés dans les cases d'un tableau : quand ces cases elles-mêmes contiennent des vecteurs, le partage de ces vecteurs implique qu'une modification de l'un se répercute sur les autres (voir également la question suivante).
Comment faire des tableaux à deux dimensions ?
La seule manière de procéder est de créer un tableau de
tableaux (les tableaux de Caml sont unidimentionnels: ce ne sont
que des vecteurs au sens mathématique).
Si l'on écrit naïvement la création d'un tableau à plusieurs
dimensions, le résultat n'est pas celui attendu, car on crée
sans le vouloir un partage physique des lignes du tableau (voir
aussi l'entrée précédente) :
La solution est d'utiliser la primitivem
let m = Array.make 2 (Array.make 3 0);; m : int array array = [|[|0; 0; 0|]; [|0; 0; 0|]|] m.(0).(0) <- 1;; - : unit = () m;; - : int array array = [|[|1; 0; 0|]; [|1; 0; 0|]|]En effet l'allocation d'un tableau consiste à calculer la valeur d'initialisation et à la mettre dans chaque case du tableau (c'est pourquoi la ligne de la matrice qui est allouée par l'expression
Array.make 3 0
est unique et
physiquement partagée par toutes les cases du tableau
m
.
La solution est d'utiliser la primitivem
make_matrix
, qui fabrique directement une matrice
dont les cases sont remplies par la valeur d'initialisation
fournie. L'autre solution consiste à écrire le programme qui
allouera explicitement une nouvelle ligne pour chaque ligne du
tableau. Par exemple:
let matrix n m init =
let result = Array.make n (Array.make m init) in
for i = 1 to n - 1 do
result.(i) <- Array.make m init
done;
result;;
matrix : int -> int -> 'a -> 'a array array = <fun>
De même, la fonction Array.copy
ne donne pas le
résultat attendu pour des matrices: il faut écrire une fonction
de copie qui copie explicitement le contenu de toutes les lignes
de la matrice:
let copy_matrix m = let l = Array.length m in if l = 0 then m else let result = Array.make l m.(0) in for i = 0 to l - 1 do result.(i) <- Array.copy m.(i) done; result
Définitions de types
Comment définir un type énuméré ?
Un type énuméré est un type somme avec uniquement des
constructeurs constants. Par exemple, voici un type avec 3
constantes :
type color = Blue | White | Red;; type color = Blue | White | Red Blue;; - : color = BlueLes noms
Blue
, White
et Red
sont des constructeurs du type color
. On peut
définir des fonctions sur ce type par filtrage :
let string_of_color = function Blue -> "blue" | White -> "white" | Red -> "red";;
Comment partager une étiquette entre deux types
enregistrement ?
Lorsqu'on définit des types qui partagent un label, le label
du deuxième type cache celui du premier. Par exemple :
type point_3d = {x : float; y : float; z : float};; type point_2d = {x : float; y : float};; {x = 10.; y = 20.; z = 30.};; The record field label z belongs to the type point_3d but is here mixed with labels of type point_2dLe moyen le plus simple de contourner cette difficulté est ... d'employer des noms différents ! Par exemple
type point3d = {x3d : float; y3d : float; z3d : float};; type point2d = {x2d : float; y2d : float};;Avec OCaml, on peut proposer deux autres solutions. Tout d'abord, il est possible d'utiliser les modules pour définir les deux types dans des espaces de noms différents:
module D3 = struct type point = {x : float; y : float; z : float} end;; module D2 = struct type point = {x : float; y : float} end;;Les labels peuvent être maintenant qualifiés avec le nom de leur module, et
D3.x
est différencié de
D2.x
:
{D3.x = 10.; D3.y = 20.; D3.z = 30.};; - : D3.point = {D3.x = 10.000000; D3.y = 20.000000; D3.z = 30.000000} {D2.x = 10.; D2.y = 20.};; - : D2.point = {D2.x = 10.000000; D2.y = 20.000000}On peut aussi adopter les objets, qui permettent la surcharge des noms de méthodes :
class point_3d ~x ~y ~z = object method x : float = x method y : float = y method z : float = z end;; class point_2d ~x ~y = object method x : float = x method y : float = y end;;En ce cas on obtient plus que la surcharge: on peut définir des fonctions polymorphes qui travaillent aussi bien sur les points de dimension 2 que de dimension 3, et l'on peut aussi considérer les points 3D comme des points 2D (à l'aide d'une simple coercion de type).
Comment définir deux types sommes qui partagent un nom de
constructeur ?
En règle générale, on ne peut pas. Il faut donc utiliser des
noms de constructeurs uniques dans tout le module qu'on écrit. À
moins bien sûr de définir les deux types en question dans deux
modules différents. Comme dans le cas des étiquettes (discuté
ci-dessus), on obtient alors des constructeurs qu'on peut
différencier en les qualifiant par le module auquel ils
appartiennent.
En OCaml, une autre solution consiste à utiliser des
« variantes polymorphes », c'est-à-dire des constructeurs qui
ne sont pas définis lors de la définition du type (ces
constructeurs sont en quelque sorte préexistants). Par
exemple :
type ids = [ `Name | `Val ];; type person = [ `Name of string ];; let (f : person -> string) = function `Name s -> s;; val f : person -> string = <fun> let (is_name : ids -> bool) = function `Name -> true | _ -> false;; val is_name : ids -> bool = <fun>
Fonctions et procédures
Comment définir une fonction ?
En Caml, les définitions de fonctions sont très proches de la
syntaxe des mathématiques: on introduit la définition par le
mot-clef
let
, suivi du nom de la fonction et de ses
arguments; enfin la formule de calcul de l'image de l'argument
est introduite par un signe =
.
let successor (n) = n + 1;;
successor : int -> int = <fun>
En fait, les parentèses autour de l'argument peuvent être
omises, de telle sorte qu'on écrit en général :
let successor n = n + 1;;
successor : int -> int = <fun>
Comment définir une fonction récursive ?
Il faut explicitement signaler la récursion: on utilise « let
rec » au lieu de « let ». Par exemple :
let rec fact n = if n = 0 then 1 else n * fact (n - 1);; fact : int -> int = <fun> let rec fib n = if n <= 1 then n else fib (n - 1) + fib (n - 2);; fib : int -> int = <fun>Les fonctions peuvent être mutuellement récursives :
let rec odd n = if n = 0 then true else if n = 1 then false else even (n - 1) and even n = if n = 0 then false else if n = 1 then true else odd (n - 1);; odd : int -> bool = <fun> even : int -> bool = <fun>
Comment appliquer une fonction ?
On applique les fonctions comme en mathématiques en écrivant
le nom de la fonction suivi de son argument entre parenthèses:
Lorsque l'expression argument d'une fonction est plus complexe qu'un simple identificateur, il faut mettre cet argument entre parenthèses. Cela est valable en particulier si l'argument d'une fonction est un nombre négatif: il faut le parenthéser. L'application de
f (x)
. En pratique les parenthèses ne sont
obligatoires que lorsque
l'expression argument x
est complexe. On les omet donc
lorsqu'il s'agit d'une constante ou d'une variable: on écrit
fib 2
au lieu de fib (2)
et fact x
au lieu de fact (x)
.
Lorsque l'expression argument d'une fonction est plus complexe qu'un simple identificateur, il faut mettre cet argument entre parenthèses. Cela est valable en particulier si l'argument d'une fonction est un nombre négatif: il faut le parenthéser. L'application de
f
à -1
s'écrit donc
f (-1)
et non
f -1
qui est interprété exactement comme
f - 1
(c'est donc une soustraction).
Comment définir une procédure ?
Rappelons qu'on appelle traditionnellement procédure,
toute commande qui produit un effet (par exemple une
impression au terminal ou la modification d'une case de la
mémoire) mais n'a pas de résultat mathématiquement
significatif.
En Caml il n'y a pas à proprement parler de différence entre procédure et fonction: une procédure est un cas particulier de fonction qui a pour résultat la valeur triviale « rien » c'est-à-dire
Les procédures qui ne nécessitent pas d'argument significatif ont encore une fois
Les procédures sont définies exactement comme des fonctions. Par exemple:
En Caml il n'y a pas à proprement parler de différence entre procédure et fonction: une procédure est un cas particulier de fonction qui a pour résultat la valeur triviale « rien » c'est-à-dire
()
. Par exemple
la primitive print_string
qui imprime une chaîne de
caractères au terminal ne rend que ()
pour tout
résultat, indiquant ainsi qu'elle a effectué le travail
demandé.
Les procédures qui ne nécessitent pas d'argument significatif ont encore une fois
()
pour argument. Par exemple,
la procédure print_newline
qui passe une ligne au
terminal n'a pas d'argument significatif: elle est de type
unit -> unit
.Les procédures sont définies exactement comme des fonctions. Par exemple:
let message s = print_string s; print_newline();; message : string -> unit = <fun> message "Hello world!";; Hello world! - : unit = ()
Comment définir une procédure/fonction qui ne prend aucun
argument ?
Remarquez qu'il est impossible de définir en Caml une
procédure sans argument: sa définition donnerait lieu à son
exécution, et on ne pourrait plus l'appeler ultérieurement. Dans
l'exemple suivant
double_newline
vaut
()
, et son évaluation ne produit pas de retour
chariots.
let double_newline = print_newline(); print_newline();; @ @ double_newline : unit = () double_newline;; - : unit = ()La définition et l'utilisation correcte de la procédure serait :
let double_newline () = print_newline(); print_newline();; double_newline : unit -> unit = <fun> double_newline;; - : unit -> unit = <fun> double_newline ();; @ @ - : unit = ()
Comment définir une function à plusieurs arguments ?
Il suffit d'écrire la liste des arguments successifs lors de
la déclaration de la fonction. Par exemple :
let sum x y = x + y;;
sum : int -> int -> int = <fun>
et de fournir les arguments dans le bon ordre à
l'application :
sum 1 2;;
- : int = 3
Ces fonctions sont appelées fonctions curryfiées, par
opposition aux fonctions ayant des n-uplets pour
argument :
let sum' (x, y) = x + y;; sum' : int * int -> int = <fun> sum' (1, 2);; - : int = 3
Comment définir une fonction qui a plusieurs résultats ?
Vous pouvez définir une fonction qui retourne une paire ou un
n-uplet :
let div_mod x y = (x quo y, x mod y);; div_mod : int -> int -> int * int = <fun> div_mod 15 7;; - : int * int = 2, 1
Qu'est-ce qu'une « fonction anonyme » ?
Une fonction peut n'avoir pas de nom: on parle alors d'une
valeur fonctionnelle ou d'une fonction anonyme. Une valeur
fonctionnelle est introduite par le mot clef
function
, suivi de l'argument de la fonction, d'une
flèche ->
et du corps de la fonction. Par
exemple :
function x -> x + 1;; - : int -> int = fun (function x -> x + 1) 2;; - : int = 3
Quelle est la différence entre
fun
et
function
?
Les fonctions s'introduisent normalement par le mot-clé
Le mot-clé
function
. Chaque paramètre de la fonction est
introduit par une nouvelle construction function
.
Par exemple :
function x -> function y -> ...définit une fonction à deux paramètres
x
et
y
. Les fonctions qui procèdent par filtrage
s'introduisent également par le mot-clé
function
.
Le mot-clé
fun
introduit des fonctions curryfiées
(à plusieurs arguments successifs). Par exemple :
fun x y -> ...introduit une fonction à deux arguments
x
et
y
comme la précédente.
Ma fonction n'est jamais appliquée
Cela provient la plupart du temps d'un argument oublié: du
fait du caractère fonctionnel de Caml, il n'y a pas d'erreur
lorsqu'on demande l'évaluation d'une fonction sans tous ses
arguments: dans ce cas une valeur fonctionnelle est retournée,
mais la fonction n'est évidemment pas appelée. Exemple
caricatural:
print_newline
sans argument ne provoque
pas d'erreur mais ne fait rien. Un avertissement est néanmoins
émis par le compilateur dans les cas flagrants de mauvaise
utilisation.
#print_newline;; - : unit -> unit #print_newline ();; @ - : unit = ()
Filtrage
Comment emboîter des filtrages ?
Il faut absolument parenthéser un filtrage qui se trouve à
l'intérieur d'un autre filtrage. En effet le filtrage le plus
interne « capture » toutes les clauses de filtrage qui le
suivent. Par exemple :
let f = function | 0 -> match ... with | a -> ... | b -> ... | 1 -> ... | 2 -> ...;;est compris comme
let f = function | 0 -> match ... with | a -> ... | b -> ... | 1 -> ... | 2 -> ...;;Cette erreur peut se produire dans tous les cas de constructions qui impliquent du filtrage:
function
, match .. with
et try ... with
. En général on parenthèse le
filtrage fautif à l'aide des mots-clefs begin
et
end
. On écrira donc :
let f = function | 0 -> begin match ... with | a -> ... | b -> ... end | 1 -> ... | 2 -> ...;;
Exceptions
Typage
Erreur : Un type est incompatible avec lui-même.
On obtient parfois le message: cette expression est de type
« un certain type » mais est utilisée avec « le même un
certain type ». Ceci se produit souvent lorsqu'on
travaille dans le système interactif.
La raison que deux types ayant le même nom a été déclaré. Le compilateur ne les confond pas, mais ne peut faire autrement que les imprimer de la même manière puisqu'ils ont le même nom. Considérez par exemple:
La raison que deux types ayant le même nom a été déclaré. Le compilateur ne les confond pas, mais ne peut faire autrement que les imprimer de la même manière puisqu'ils ont le même nom. Considérez par exemple:
type t = T of int type t = T of int let x = T 1;; val x : t = T 1 type t = T of int;; type t = T of int let incr = function T x -> T (x+1);; val incr : t -> t = <fun> incr x;; This expression has type t but is here used with type tCe phénomène apparaît souvent lorsqu'on utilise le système interactif en rechargeant de multiples fois le même fichier (ce qui redéfinit les types de données). La solution est alors de recommencer une nouvelle session.
Une fonction obtenue par application partielle n'est pas suffisamment polymorphe
Le cas le plus fréquent où une définition n'est pas assez
polymorphe se produit lors de la définition d'une fonction
par application partielle d'une fonction polymorphe plus
générale. En Caml le polymorphisme est introduit uniquement
par la construction « let », et le résultat d'une
application est seulement faiblement polymorphe. En
conséquence la fonction résultant de l'application ne peut
pas être polymorphe.
Ici il suffit de définir la fonction en rendant la
fonctionnalité explicite pour le typeur : en général on
ajoute une construction
function
ou un paramètre
supplémentaire (cette transformation est appelée
eta-expansion):
let map_id = List.map (function x -> x) (* Result is weakly polymorphic *) val map_id : '_a list -> '_a list = <fun> map_id [1;2] - : int list = [1;2] map_id (* No longer polymorphic *) - : int list -> int list = <fun> let map_id' l = List.map (function x -> x) l val map_id' : 'a list -> 'a list = <fun> map_id' [1;2] - : int list = [1;2] map_id' (* Still fully polymorphic *) - : 'a list -> 'a list = <fun>La nouvelle définition est sémantiquement équivalente à l'ancienne, et peut recevoir un schéma de type polymorphe étant donné que ce n'est plus une application.
Le type d'une expression contient des variables de types qui
ne peuvent pas être généralisées
Ce message est émis lorsque le compilateur Caml essaie de
compiler une fonction ou une valeur qui est monomorphe, mais
pour laquelle certains types n'ont pu être complètement
inferrés. Des variables de types apparaissent alors dans le
type, qu'on appelle « faibles » (et qu'on marque par
un souligné :
'_a
). Elles disparaitront
grâce au mécanisme d'inférence de types des que suffisamment
d'informations auront pu être rassemblées.
let r = ref [] val r : '_a list ref = {contents = []} let f = List.map (fun x -> x) val f : '_a list -> '_a list = <fun>L'expression fautive ne pouvant pas être compilée en l'état, il faut envisager deux cas :
- L'expression ne peut pas être rendue polymorphe,
comme dans le cas de
r
au-dessus. L'utilisateur doit utiliser une annotation de type explicite pour en faire une expression complètement monomorphe. - L'expression peut être rendue polymorphe en
réécrivant certaines parties du code (par exemple
en utilisant l'eta-expansion),
comme dans le cas de
f
.
Comment écrire une fonction qui reçoit un argument
polymorphe ?
En ML, l'argument d'une fonction ne peut pas être
polymorphe à l'intérieur du corps de la fonction, ce
qui explique le typage suivant :
En OCaml il est néanmoins possible d'utiliser du polymorphisme de premier ordre. Pour cela on peut utiliser des enregistrements ou des objets; dans le cas des enregistrements il faut déclarer le type avant de l'utiliser dans la fonction.
let f (g : 'a -> 'a) x y = g x, g y
val f : ('a -> 'a) -> 'a -> 'a -> 'a * 'a = <fun>
La fonction n'est pas aussi polymorphe qu'on aurait
pu l'espérer.En OCaml il est néanmoins possible d'utiliser du polymorphisme de premier ordre. Pour cela on peut utiliser des enregistrements ou des objets; dans le cas des enregistrements il faut déclarer le type avant de l'utiliser dans la fonction.
let f (o : <g : 'a. 'a -> 'a>) x y = o#g x, o#g y val f : < g : 'a. 'a -> 'a > -> 'b -> 'c -> 'b * 'c = <fun> type id = { g : 'a. 'a -> 'a; } type id = { g : 'a. 'a -> 'a; } let f r x y = r.g x, r.g y val f : id -> 'a -> 'b -> 'a * 'b = <fun>
Entrées-sorties
Pourquoi certaines sorties ne sont-elles pas dans le bon
ordre ?
Si vous utilisez le module
format
, vous ne
devez pas mélanger les ordres d'impression de
format
avec les ordres d'impression habituels,
car les ordres d'impression de format
sont
retardées pour faire la mise en page et les coupures de
lignes, alors que les impressions habituelles ne sont pas
soumises à ce retard.
print_string "before";
Format.print_string "MIDDLE";
print_string "after";;
beforeafterMIDDLE- : unit = ()
Pour éviter ce problème, il ne faut pas mélanger les
ordres d'impression de format
et les ordres de
plus bas niveau; c'est pourquoi on a l'habitude d'ouvrir le
module format
lorsqu'on utilise ses
fonctions.