Introduction à Objective Caml

Maxence Guesdon

6 février 2012

Table des matières

Préambule

Cette formation est une introduction au langage Objective Caml, aussi appelé OCaml. Une partie du plan de cette formation est basée sur le plan du livre "Développement d'applications avec Objective Caml" d'Emmanuel Chailloux, Pascal Manoury et Bruno Pagano (cf. section 7.2). Certains paragraphes sont repris verbatim de cet ouvrage, avec l'accord des auteurs, et apparaissent «ainsi».

OCaml est un langage très riche offrant de nombreuses possibilités. De plus, les différents outils de développement fournis en standard sont très puissants et flexibles. Enfin, la bibliothèque standard recouvre beaucoup de domaines (interface système, structures de données, parallélisme, interface graphique, …).

Nous nous contenterons de présenter les bases permettant de commencer à programmer en OCaml, de façon à nous concenter sur l'essentiel sans nous perdre dans les détails. Par exemple, toutes les options de compilation ne sont pas explicitées et l'on se reportera au manuel de référence pour une utilisation plus poussée. Toutes les bibliothèques ne seront pas non plus utilisées dans cette formation.

L'objectif de cette formation est d'acquérir les bases syntaxiques et sémantiques du langage ainsi que la connaissance des bibliothèques et outils de développement disponibles pour approfondir ensuite selon ses besoins.

Chapitre 1  Introduction

«Objective Caml est un langage récent qui se place dans l'histoire des langages de programmation comme un lointain descendant de LISP ayant su tirer les enseignements de ses cousins en incorporant les principales caractéristiques des autres langages. Il est développé à l'INRIA et s'appuie sur une longue expérience de conception de langages de la famille ML. Objective Caml est généraliste pour l'expression d'algorithmes symboliques ou numériques. Il est orienté objet et possède un système de modules paramétrés. Il permet le développement d'applications concurrentes ou distribuées. Il possède une excellente sûreté à l'exécution grâce à son typage statique, son mécanisme d'exceptions et son récupérateur automatique de mémoire. Il est performant tout en étant portable. Enfin, il dispose d'un riche environnement de développement.»

OCaml est développé et maintenu principalement par des chercheurs et ingénieurs de l'INRIA (équipe-projet Gallium, SED de Rocquencourt, Pierre Weis de l'équipe-projet Estime) ainsi que par des chercheurs extérieurs comme Jacques Garrigue (Université de Nagoya, au Japon) et Alain Frisch (société Lexifi).

1.1  Description du langage

Nous listons ici les traits principaux d'Objective Caml. Tous ces aspects ne sont pas abordés dans cette formation. Ceux qui le sont figurent en gras.

1.2  Evaluation, compilation, exécution

Il existe plusieurs façons d'exécuter un programme OCaml :

1.2.1  Utilisation de l'interprète (toplevel)

Il est possible d'interpréter le code d'un programme, de façon interactive ou non, grâce au toplevel. Le toplevel OCaml permet de saisir et exécuter interactivement du code OCaml, de la même façon que dans un interprète de commandes UNIX (shell). L'inférence et la vérification des types sont faites et le code exécuté immédiatement. Ce mode permet de tester facilement de petits bouts de programme, sans avoir à compiler puis exécuter toute une application.
Le toplevel est lancé par la commande ocaml. L'utilisateur peut alors saisir des phrases OCaml terminées par ";;" qui sont évaluées immédiatement. Le toplevel affiche les types et valeurs des expressions évaluées avant de réafficher le prompt :

# "Hello world!" (* code saisi par l'utilisateur *);;
- : string = "Hello world!"
# 42.0;;
- : float = 42.

Il est possible de charger des modules additionnels compilés (cf. chapitre 5) soit en donnant ces modules sur la ligne de commande :

$ ocaml mon_module.cmo mon_module2.cmo
soit en utilisant des directives de chargement #load à l'invite du toplevel :

# #load "mon_module.cmo";;
# #load "mon_module2.cmo";;

Le contenu des modules chargés est alors accessible depuis le code saisi dans le toplevel.

Il est également possible de lancer le toplevel sur un fichier de code OCaml, ce dernier sera évalué comme si son contenu était saisi à l'invite du toplevel :

$ ocaml mon_module.ml

Pour faciliter la saisie de code dans le toplevel, on pourra utiliser l'outil ledit1 qui offre des fonctionnalités d'édition de ligne de commande (historique, utilisation des flèches du clavier, …) de la façon suivante :

$ ledit ocaml

Il existe également un toplevel graphique, topcameleon2, permettant de sauvegarder et recharger le code saisi ainsi que d'afficher la structure des valeurs calculées.

Pour la suite de la formation, nous utiliserons un éditeur et ferons exécuter notre code par la commande

$ ocaml notre_fichier.ml

Pour en savoir plus sur le toplevel, consulter le manuel :
http://caml.inria.fr/pub/docs/manual-ocaml/manual023.html
http://www.pps.jussieu.fr/Livres/ora/DA-OCAML/

1.2.2  Compilation vers du code-octet

Il existe deux compilateurs OCaml différents, l'un compilant vers du code-octet (byte-code), l'autre vers du code natif.

Le code-octet peut être exécuté par une machine virtuelle OCaml sur d'autres architectures que celle pour laquelle il a été compilé. C'est le même principe que le code-octet pour machine virtuelle Java. Des performances moindres sont la contrepartie de cette portabilité. Par ailleurs, dans le cas d'utilisation de bibliothèques C, ces bibliothèques doivent être présentes sur la machine d'exécution.

Les principales extensions des fichiers utilisées par les compilateurs OCaml et leurs significations sont indiquées dans le tableau de la figure 1.1.


.mlSource d'implémentation (cf. chapitre 5, équivalent des .c en C)
.mliSource d'interface (cf. chapitre 5, sorte d'équivalent des .h en C)
.cmoImplémentation compilée en code-octet
.cmiInterface compilée
.cmx et .oImplémentation compilée en code natif
.cmaBibliothèque (regroupement de plusieurs implémentations compilées) en code-octet
.cmxa et .aBibliothèque en code natif
Figure 1.1: Principales extensions des noms de fichiers utilisées par les compilateur OCaml.

Le compilateur code-octet est ocamlc, invoqué par des commandes de la forme suivante :

$ ocamlc [options] fichiers

Les options les plus courantes sont les suivantes :

Beaucoup d'autres options de compilation existent. Nous ne présentons ici que les options de base permettant de compiler des programmes simples. On se réfèrera donc au manuel pour connaître toutes les options et possibilités du compilateur :
http://caml.inria.fr/pub/docs/manual-ocaml/manual022.html

1.2.3  Compilation vers du code natif

La compilation vers du code natif permet d'obtenir de bien meilleures performances pour l'exécutable produit. Cependant, l'exécutable obtenu ne peut pas être exécuté sur d'autres architectures que celle sur laquelle il a été compilé.

Le compilateur vers du code natif est ocamlopt. Il s'utilise de la même façon que le compilateur ocamlc (cf. section 1.2.2), mais utilise des extensions différentes pour les fichiers utilisés et produits (cf. figure 1.1).

La documentation relative au compilateur vers du code natif se trouve ici :
http://caml.inria.fr/pub/docs/manual-ocaml/manual025.html

1.3  Le site web

Le site http://caml.inria.fr/ est le site officiel d'Objective Caml et on y trouve, en plus des distributions, tout le matériel nécessaire pour débuter en OCaml : documentations, références de livres, tutoriels, liens, ….

1.4  Accès aux documentations

Les documentations de base pour développer en OCaml sont les suivantes :


1
2

Chapitre 2  Programmation fonctionnelle

La syntaxe d'OCaml et le paradigme de programmation fonctionnelle permettent de mieux voir un programme comme une fonction retournant une valeur calculée à partir de paramètres. Cela se rapproche des définitions mathématiques des fonctions, par opposition aux descriptions opérationnelles que l'on trouve dans les langages impératifs.

Illustrons cela avec l'exemple de la fonction factorielle. Une définition mathématique possible est la suivante :

fact: ℕ+→ℕ+;    fact(0) = 1 ;   fact(n) = n * fact(n−1)

En OCaml, cette fonction sera définie comme ci-dessous ; on notera la ressemblance de syntaxe entre les deux définitions.

let rec fact = function
0 -> 1
| n -> n * fact (n-1);;

En C, cette fonction pourrait s'écrire :

unsigned int fact (unsigned int n) {
unsigned int i = 1, res = 1;

while (i <= n)
res = res * i++;
return res;
}

2.1  Noyau fonctionnel

Le noyau fonctionnel du langage permet d'appliquer des fonctions à des valeurs. Les valeurs sont typées et peuvent être de types de base (entier, flottant, …., cf. section 2.1.1), de types plus élaborés construits à partir d'autres types, ou bien de type fonction. En effet, en OCaml, les fonctions sont des valeurs de premier ordre, c'est-à-dire que le langage permet de les manipuler comme d'autres valeurs "simples" comme les entiers. En particulier, on peut définir des fonctions et les passer en paramètres d'autres fonctions.

2.1.1  Valeurs, fonctions et types de base

Types de base

Les types ci-dessous sont prédéfinis en OCaml. On donne pour chacun la syntaxe et des exemples de manipulation.

Les entiers (type int)

# 1 (* l'entier 1 *);;
- : int = 1
# 1 + 0xde (* l'addition avec l'opérateur '+', 0xde est la notation hexadécimale*) ;;
- : int = 223
# 3 - 0o11 (* la soustraction, 0o11 est la notation octale *) ;;
- : int = -6
# 4 * 0b10101 (* la multiplication, 0b10101 est la notation binaire *) ;;
- : int = 84
# 10 / 3 (* la division entière *) ;;
- : int = 3
# (/) 10 3 (* idem, en utilisant l'opérateur en position préfixe *) ;;
- : int = 3
# 15000003 mod 4 (* le modulo, reste de la division entière *) ;;
- : int = 3
# 15_000_003 mod 4 (* même opération, avec un entier plus lisible *) ;;
- : int = 3
# max_int (* le plus grand entier disponible *) ;;
- : int = 4611686018427387903
# min_int - 1 (* attention, pas de contrôle sur le dépassement des limites *) ;;
- : int = 4611686018427387903

max_int et min_int dépendent de l'architecture de la machine. Sur une machine 32 bits, les entiers natifs sont codés sur 31 bits, sur 63 bits sur une machine 64 bits. Cela est dû à la façon dont sont encodées les valeurs.

Il existe beaucoup d'autres opérations sur les entiers, incluant également la manipulation au niveau bits : http://caml.inria.fr/pub/docs/manual-ocaml/libref/Pervasives.html#6_Integerarithmetic

On remarquera que l'interprète a automatiquement déterminé le type des expressions saisies (ici, int). C'est ce que l'on appelle l'inférence de type : le typeur a déduit d'après le code les types des expressions évaluées. Nous verrons d'autres exemples d'inférence de type au fut et à mesure de la formation.

Les flottants (type float)

# 1.0 (* le réel 1.0 *);;
- : float = 1.
# 1.0e+5 (* notation avec exposant *) ;;
- : float = 100000.
# 1.2 +. 1. (* l'addition avec l'opérateur '+.' *) ;;
- : float = 2.2
# 3.3 -. 2.15 (* la soustraction *) ;;
- : float = 1.15
# 4. *. 3. (* la multiplication *) ;;
- : float = 12.
# 10. /. 3. (* la division *) ;;
- : float = 3.33333333333333348
# (/.) 10. 3. (* idem, en utilisant l'opérateur en position préfixe *) ;;
- : float = 3.33333333333333348
# mod_float 15000003. 4.5 (* le modulo *) ;;
- : float = 0.
# mod_float 15_000_003. 4.5 (* même opération, avec un flottant plus lisible *) ;;
- : float = 0.
# 1.999999999 +. 100000000. (* la précision est celle de la norme IEEE 754 *);;
- : float = 100000002.
# ceil 3.5 (* arrondi à l'entier supérieur *) ;;
- : float = 4.
# floor 3.5 (* arrondi à l'entier inférieur *) ;;
- : float = 3.
# truncate 3.5 (* ne conserve que la partie entière *) ;;
- : int = 3
# 1. /. 0. (* la division par zéro donne l'infini, ce qui
n'est pas un nombre, 'not a number' ou 'NaN' *);;
- : float = infinity

D'autres opérations sont disponibles sur les flottants; on consultera le manuel pour les découvrir : http://caml.inria.fr/pub/docs/manual-ocaml/libref/Pervasives.html#6_Floatingpointarithmetic.

Les opérateurs + et +. ne sont pas génériques et s'appliquent respectivement sur des entiers et des flottants (idem pour les autres opérateurs exposés plus haut) :

# 1 + 2.0 ;;
File "", line 1, characters 4-7:
Error: This expression has type float but an expression was expected of type
int
# 1 +. 2.0 ;;
File "", line 1, characters 0-1:
Error: This expression has type int but an expression was expected of type
float

Les booléens (type bool)

# true (* la valeur 'vrai' *) ;;
- : bool = true
# false (* la valeur 'faux' *) ;;
- : bool = false
# true && false (* ET paresseux sequentiel, evalue de gauche a droite *) ;;
- : bool = false
# true || false (* OU paresseux sequentiel, evalue de gauche a droite *) ;;
- : bool = true
# not true (* NON logique *) ;;
- : bool = false

Les caractères (type char)

# 'a' (* le caractere 'a' *) ;;
- : char = 'a'
# int_of_char 'a' (* son code ASCII *) ;;
- : int = 97
# char_of_int 123 (* le caractere de code ASCII 123 *) ;;
- : char = '{'

D'autres fonctions sont disponibles dans le module Char .
Les chaînes de caractères (type string)

# "hello world!";;
- : string = "hello world!"
# "hello " ^ "world!" (* concatenation *);;
- : string = "hello world!"
# String.length "hello world!" (* longueur de chaine *);;
- : int = 12
# "hello world!".[4] (* acces à un caractere, indice commencant a zero *) ;;
- : char = 'o'

Beaucoup d'autres fonctions sont disponibles dans le module String .

Attention

Il n'existe pas de conversion automatique d'un type vers un autre comme dans beaucoup d'autres langages interprétés. Ainsi :

# string_of_int 25 (* construction d'une chaine representant l'entier 25 *) ;;
- : string = "25"
# 3 + "25";;
File "", line 1, characters 4-8:
Error: This expression has type string but an expression was expected of type
int

Le type unit
«Le type unit décrit un ensemble ne possédant qu'une seule valeur, notée ()

# () ;;
- : unit = ()

«Cette valeur sera plus particulièrement utilisée dans les programmes impératifs (cf. section 3) pour les fonctions qui effectuent des effets de bord. Les fonctions dont le résultat est la valeur () simulent la notion de procédure qui n'existe pas en OCaml comme le fait le type void dans le langage C.»

Les n-uplets
Les n-uplets permettent de grouper plusieurs valeurs, potentiellement de types différents. Un n-uplet à 2 éléments est aussi appelé paire. Voici quelques exemples de constructions de n-uplets :

# (1, 2) (* paire d'entiers *) ;;
- : int * int = (1, 2)
# (1, "2") (* paire composee d'un entier et d'une chaine *) ;;
- : int * string = (1, "2")
# fst (1, 2) (* acces a la premiere composante de la paire *) ;;
- : int = 1
# snd (1, 2) (* acces a la seconde composante *);;
- : int = 2
# (1, 2, 3) (* n-uplet de 3 elements *) ;;
- : int * int * int = (1, 2, 3)
# (1.0, "hello", "world", '!') (* n-uplet de 4 elements de 3 types differents *) ;;
- : float * string * string * char = (1., "hello", "world", '!')

On notera que les types des n-uplets inférés sont notés par une '*', comme dans int * string * string * char.

Attention

Les fonctions fst et snd ne s'appliquent qu'à des paires et non à des n-uplets de taille supérieure, comme l'indique le type de ces fonctions :

# fst;;
- : 'a * 'b -> 'a = <fun>
# snd;;
- : 'a * 'b -> 'b = <fun>
# fst (1, 2, 3);;
File "", line 1, characters 4-13:
Error: This expression has type int
* int * int
but an expression was expected of type 'a * 'b

La notation 'a * 'b -> 'a indique qu'il s'agit d'un type fonctionnel (aussi appelé type "flèche"), décrivant une fonction prenant en argument une paire d'éléments de types quelconques ('a * 'b) et renvoyant une valeur de même type que le premier élément de la paire ('a).

Les listes (type 'a list)
Le type list est prédéfini et permet de construire des listes d'éléments de même type. C'est un type paramétré, 'a indique le paramètre de type, qui est peut être instancié selon le type des éléments de la liste :

# [] (* une liste vide, donc sans contrainte sur le type des elements *) ;;
- : 'a list = []
# [ 1 ] (* une liste d'entiers a un element *) ;;
- : int list = [1]
# [ 1 ; 2 ; 3 ] (* une liste de 3 entiers *) ;;
- : int list = [1; 2; 3]
# [ "hello" ; "world" ; "!" ] (* une liste de chaines *) ;;
- : string list = ["hello"; "world"; "!"]
# [ 1 ; "2" ; 3 ] (* liste invalide car les elements n'ont pas le meme type *) ;;
File "", line 1, characters 6-9:
Error: This expression has type string but an expression was expected of type
int

Voyons quelques exemples de manipulation de listes :

# 1 :: 2 :: 3 :: [] (* ajout d'element en tete de liste par l'operateur 'cons' :: *) ;;
- : int list = [1; 2; 3]
# [ 1 ; 2 ] @ [ 3 ; 4 ; 5] (* concatenation de deux listes (de meme type) *) ;;
- : int list = [1; 2; 3; 4; 5]
# List.length [ "hello" ; "world" ; "!" ] (* acces a la longeur *) ;;
- : int = 3
# List.hd [ 1 ; 2 ; 3 ] (* recuperation de l'element de tete de liste *) ;;
- : int = 1
# List.tl [ 1 ; 2 ; 3 ] (* recuperation de la queue de la liste (i.e. sans la tete) *) ;;
- : int list = [2; 3]
# String.concat "-" [ "hello" ; "world" ; "!" ] (* concatenation d'une liste de chaines *) ;;
- : string = "hello-world-!"

Beaucoup d'autres fonctions sont disponibles dans le module List .

2.1.2  Comparaisons

OCaml offre des fonctions de comparaisons polymorphes, c'est-à-dire opérant sur n'importe quels types de données, à la seule condition que les valeurs comparées soient du même type. Elles sont donc de type 'a -> 'a -> bool, indiquant clairement que le type des deux arguments doit être le même.

Voyons ces fonctions :

# 1 = 2 (* egalite structurelle sur des entiers *) ;;
- : bool = false
# "hello" = "world" (* ... ou sur des chaines *) ;;
- : bool = false
# (1, 2, 3) = (1, 2, 4) (* ... ou des n-uplets *) ;;
- : bool = false
# [ 1 ; 2 ; 3 ] <> [ 1 ; 2 ; 4 ] (* différence structurelle, ici sur des listes *) ;;
- : bool = true
# not ([ 1 ; 2 ; 3 ] = [ 1 ; 2 ; 4 ])
(* idem que ci-dessus, en utilisant la negation de l'egalite *) ;;
- : bool = true
# 1 < 2 (* inferiorite (structurelle) stricte *) ;;
- : bool = true
# 1 <= 2 (* inferieur ou egal *) ;;
- : bool = true
# "hello" > "world" (* superieur *) ;;
- : bool = false
# [ 1 ; 2 ; 3 ] >= [ 1 ; 2 ] (* superieur ou egal *) ;;
- : bool = true
# 1 == 2 (* egalite physique *) ;;
- : bool = false
# 1 != 2 (* difference physique *) ;;
- : bool = true

«L'égalité structurelle teste l'égalité de deux valeurs en explorant leur structure, alors que l'égalité physique teste si les deux valeurs occupent la même zone mémoire. Ces deux égalités retournent le même résultat pour les valeurs simples : booléens, caractères, entiers et constructeurs constants».

La comparaison structurelle repose sur l'ordre des entiers pour les entiers, les chaînes et les caractères (en utilisant les codes ascii), l'ordre naturel sur les flottants.

2.1.3  Structure de contrôle conditionnelle

La syntaxe de la conditionnelle peut prendre deux formes :

La première forme, sans écriture de la branche else, est en fait évaluée comme

if <expression de type bool> then <expression> else ()

Le typage correct de l'expression toute entière impose que les expressions des branches then et else soient du même type. En effet, si ce n'était pas le cas, l'expression entière aurait un type différent selon que l'expression de condition est vraie ou fausse. L'expression toute entière est donc du même type que celui des branches.

Pour cette raison, l'expression du then de la première forme doit être de type unit, car l'expression de la branche else implicite est (), qui est de type unit.

Voyons quelques exemples :

# if true then 1 lsl 5 + 10 else 1
(* lsl est le décalage logique de bits vers la gauche *) ;;
- : int = 42
# (if 12 * 4 < 40 then 15 else 2) + (if 3 > 4 then 10 else 4 * 10)
(* une expression conditionnelle est aussi une expression dont le calcul
retourne une valeur. *);;
- : int = 42
# if true then 1;;
File "", line 1, characters 13-14:
Error: This expression has type int but an expression was expected of type
unit

La dernière expression est un exemple d'expression mal typée, car les types des branches then et else diffèrent, ce que signale l'interprète : l'expression 1 est de type entier alors qu'on attend ici une expression de type unit (comme la branche else).

2.1.4  Déclarations de valeurs

Les déclarations de valeurs sont des associations entre un nom (un identifiant) et une valeur. L'obtention de la valeur nécessite le calcul de l'expression de la définition de l'identifiant. On dit que la valeur est liée (en anglais bound) au nom dans l'environnement. Les identifiants sont de la forme suivante : [a-z_][0-9A-Za-z_']*, c'est-à-dire qu'ils peuvent contenir les caractères 'a' à 'z', 'A' à 'Z', '0' à '9', _ et ' mais doivent commencer par une lettre minuscule ou le caractère _.

Il existe deux types de déclarations : globales et locales.

Déclarations globales

La syntaxe est la suivante :

let <identifiant> = <expression>;;

La valeur résultant du calcul de l'expression est ensuite associée à l'identifiant. Cet identifiant est alors connu et peut être utilisé dans toutes les expressions qui suivent dans le programme. Il est alors remplacé dans ces expressions par sa valeur associée :

# let x = 1 ;;
val x : int = 1
# let y = 41 ;;
val y : int = 41
# let z = x + y ;;
val z : int = 42

Il est impossible d'utiliser un identifiant inconnu :

# foo + 1;;
File "", line 1, characters 0-3:
Error: Unbound value foo

Le message indique que la valeur foo n'est pas liée dans l'environnement.

On remarquera qu'il est syntaxiquement impossible de déclarer un nom sans valeur. Cela est fait exprès pour éviter des problèmes de variables non initialisées comme en C :

void foo () { int toto ; ... }

Dans la fonction C ci-dessus, on peut déjà utiliser la variable toto sans savoir à quoi elle fait référence, ce qui peut provoquer des erreurs d'exécution aléatoires.

Déclarations locales

Il est possible de déclarer localement des valeurs. Le nom de la valeur n'est alors visible que dans une seule expression. La syntaxe est la suivante :

let <identifiant> = <expression1> in <expression2>

La valeur de l'expression1 est calculée et liée à l'identifiant. Ce dernier n'est visible que dans l'expression2. Exemple :

# let foo = (* declaration globale foo *)
let bar =
let glop = 1 in (* declaration locale de glop *)
glop + 2
in (* declaration locale de bar, glop n'est plus visible *)
let gee = bar + 1 in (* declaration locale de gee, utilisant bar *)
gee + bar + 2 (* utilisation de gee et bar locales *);;
val foo : int = 9
# let buz = bar + foo (* bar n'est plus visible ici *) ;;
File "", line 1, characters 10-13:
Error: Unbound value bar

L'indentation ci-dessus permet de bien voir l'imbrication des expressions. Cependant, elle prend davantage de place en largeur. On trouvera souvent cette seconde indentation, n'ajoutant pas de décalage supplémentaire après un in :

# let foo =
let bar =
let glop = 1 in
glop + 2
in
let gee = bar + 1 in
gee + bar + 2;;
val foo : int = 9

Des conseils sur l'indentation sont donnés à l'adresse http://caml.inria.fr/resources/doc/guides/guidelines.fr.html#indentation.

Attention

Une déclaration locale (let ... = ... in ...) ne peut englober une déclaration globale (let ... = ... ;;) :

# let x = 1 in let y = 2;;
File "", line 1, characters 22-24:
Error: Syntax error

Déclaration simultanées

Il est possible de déclarer simultanément plusieurs valeurs globales :

let <ident1> = <expression1>
and
<ident2> = <expression2>
...
and
<identN> = <expressionN>;;

ou locales :

let <ident1> = <expression1>
and
<ident2> = <expression2>
...
and
<identN> = <expressionN> in
<expression>

Les valeurs ne deviennent cependant visibles qu'après toutes les déclarations simultanées :

# let x_int = 1
and x_float = 1.0;;
val x_int : int = 1
val x_float : float = 1.

# let y_int = 2
and y_float = float_of_int y_int;;
File "", line 2, characters 27-32:
Error: Unbound value y_int

Une autre façon de définir plusieurs valeurs en même temps est d'utiliser la notation des n-uplets :

# let (a, b, c) = (1, 2, 3);;
val a : int = 1
val b : int = 2
val c : int = 3

# let (a, b) = (1, 2, 3);;
File "", line 1, characters 13-22:
Error: This expression has type int
* int * int
but an expression was expected of type 'a * 'b
# let (a, b, c) = (1, 2);;
File "", line 1, characters 16-22:
Error: This expression has type int
* int
but an expression was expected of type 'a * 'b * 'c

Contrairement à d'autres langages, le compilateur veille à ne pas avoir de variable non initialisée comme cela serait le cas pour c dans let (a, b, c) = (1, 2). Les valeurs de l'expression de définition ne sont pas non plus "recyclées" comme dans le langage R (ce qui aurait donné la valeur 1 à c dans notre exemple).

Enfin, dans let (a, b) = (1, 2, 3), le compilateur nous indique clairement que la construction n'est pas correcte puisque nous avons un n-uplet de taille 3 à droite alors que nous le filtrons par un n-uplet de taille 2. Nous verrons le filtrage en détail dans la section 2.2.1.

2.1.5  Expressions fonctionnelles, fonctions

«Une expression fonctionnelle est constituée d'un paramètre et d'un corps. Le paramètre formel est un nom de variable et le corps une expression. On dit que le paramètre est abstrait. C'est pour cela qu'une expression fonctionnelle est aussi appelée abstraction.»

Définition de fonctions

On définit une fonction par la syntaxe suivante :

function <identifiant> -> <expression>

Une fonction ne prend qu'un seul paramètre. Cependant, elle peut renvoyer à son tour une valeur fonctionnelle, ce qui permet de simuler des fonctions à plusieurs paramètres :

function <identifiant1> -> function <identifiant2> -> <expression>

Voyons quelques exemples de définition de fonctions :

# function x -> x * x (* la fonction carre sur les entiers *) ;;
- : int -> int = <fun>
# function x -> function y -> x +. y (* l'addition sur les flottants *) ;;
- : float -> float -> float = <fun>
# fun x y -> x +. y (* raccourci syntaxique pour l'expression ci-dessus *);;
- : float -> float -> float = <fun>

On remarquera le type de ces expressions, inféré automatiquement par l'interprète. Les types des valeurs fonctionnelles sont noté avec ->. Ainsi, int -> int indique une valeur fonctionnelle prenant en paramètre une valeur de type int et retournant une valeur de type int.
Le type float -> float -> float est équivalent à float -> (float -> float) (associativité à droite de ->), ce qui signifie que la valeur prend en paramètre un float et retourne une valeur fonctionnelle de type float -> float qui elle aussi prend un float mais retourne une valeur de type float.

Application de fonctions

L'application d'une fonction à une valeur s'écrit en juxtaposant une valeur fonctionnelle et un paramètre :

# (function x -> x + 1) 41 ;;
- : int = 42
# ((function x -> function y -> x +. y) 1.0) 41.0 ;;
- : float = 42.
# (function x -> function y -> x +. y) 1.0 41.0 (* suppression des parentheses inutiles *) ;;
- : float = 42.

Dans la dernière expression ci-dessus, nous supprimons les parenthèses inutiles car l'application est associative à gauche, c'est à dire que le premier paramètre 1.0 est appliqué à function x -> ..., ce qui revient donc à calculer la l'expression (function y -> 1.0 +. y) (le paramètre x est remplacé par la valeur 1.0 à laquelle est appliquée la fonction); la valeur obtenue est encore une fonction qui est alors appliquée à 41.0 pour finalement calculer 1.0 +. 41.0.

Il est donc possible d'appliquer un seul paramètre à la même valeur fonctionnelle :

# (function x -> function y -> x +. y) 1.0;;
- : float -> float = <fun>

Le résultat est une expression fonctionnelle. Cet aspect est parfois appelé application partielle quand on considère la fonction comme ayant deux paramètres x et y.

L'expression function (x, y) -> ... n'est pas équivalente à function x -> function y -> .... En effet, dans la première on définit une fonction prenant en paramètre un couple, tandis que la seconde définit une fonction retournant une fonction, ce qui revient à une fonction à deux paramètres.

Le mode d'évaluation de l'application en OCaml est le passage par valeur, c'est-à-dire que l'expression en paramètre d'une fonction est d'abord évaluée et ensuite cette valeur est passée à la fonction pour évaluer cette dernière. Cela est différent du passage par nom. Ainsi, dans l'expression

# (function x -> x+1) (24 + 17);;
- : int = 42

l'expression 24 + 17 est d'abord évaluée et la valeur 41 est passée à la fonction pour en évaluer le corps.

La valeur de retour d'une fonction est la valeur de l'expression du corps de la fonction.

Les fonctions définies et appliquées "au vol" sont appelées fonctions anonymes, car elles ne sont pas liées à un identifiant. Bien souvent, on nommera les fonctions pour les appliquer plus tard, en effectuant des déclarations globales ou locales :

# let square = function x -> x * x (* fonction carre sur les entiers *) ;;
val square : int -> int = <fun>
# let add = fun x y -> x + y (* en utilisant le raccourci de syntaxe *) ;;
val add : int -> int -> int = <fun>
# let sub = function x -> function y -> x - y ;;
val sub : int -> int -> int = <fun>
# let sub x y = x - y (* idem en utilisant un autre raccourci de syntaxe *) ;;
val sub : int -> int -> int = <fun>

Enfin, le compilateur effectue des vérifications de type lors de l'application de fonction, comme nous l'avons vu précédemment sur les opérateurs arithmétiques. Voyons quelques exemples corrects et incorrects :

# square 24 ;;
- : int = 576
# square 24.0 (* erreur: le parametre doit etre un entier *) ;;
File "", line 1, characters 7-11:
Error: This expression has type float but an expression was expected of type
int
# (square 1) 2 (* erreur (square 1) n'est pas une fonction et ne peut etre appliquee *) ;;
File "", line 1, characters 0-10:
Error: This expression is not a function; it cannot be applied

# let add1 = add 1 (* application "partielle" de add pour obtenir une fonction d'increment *) ;;
val add1 : int -> int = <fun>
# add1 41 ;;
- : int = 42

Attention

Les parenthèses ne peuvent être mises n'importe où :

# let f x y = x + y ;;
val f : int -> int -> int = <fun>
# f 1 2 ;;
- : int = 3
# (f 1) 2 ;;
- : int = 3
# (f 1 2) ;;
- : int = 3
# ((f 1) 2) ;;
- : int = 3
# f ( 1 ) 2 ;;
- : int = 3
# f 1 ( 1 + 1);;
- : int = 3
# f ( 0 + 1 ) ( 2 + 0 );;
- : int = 3
# f (1 2);;
File "", line 1, characters 3-4:
Error: This expression is not a function; it cannot be applied

Fermetures

Considérons l'exemple suivant :

# let increment = 2;;
val increment : int = 2
# let add_increment x = x + increment ;;
val add_increment : int -> int = <fun>
# add_increment 1 ;;
- : int = 3
# let increment = 20 ;;
val increment : int = 20
# add_increment 1 ;;
- : int = 3

Nous constatons que la valeur retournée par add_increment 1 ne varie pas, même après avoir redéfini la variable increment.

Ce comportement, qui est celui souhaité, est obtenu grâce aux fermetures. Une fermeture (en anglais closure) est composée d'une fonction et de son environnement, c'est-à-dire les valeurs liées à des identifiants au moment de la définition de fonction.

En OCaml, la définition d'une expression fonctionnelle entraîne la création d'une fermeture pour garantir ce comportement, et c'est cette fermeture qui est retournée. Lors de l'application d'une fonction, la valeur du paramètre est associée au paramètre et le corps de la fonction est évalué dans l'environnement conservé dans la fermeture enrichi par la valeur liée au paramètre.

Opérateurs infixes

Nous avons vu comment définir des fonctions préfixes, c'est-à-dire qu'elles sont positionnées avant les expressions sur lesquelles elles sont appliquées, comme dans add 30 12.

Il est parfois plus lisible d'avoir des fonctions s'appliquant à deux paramètres et dont l'application s'écrira plus naturellement avec le nom de la fonction entre ses paramètres. C'est le cas des opérateurs arithmétiques vus précédemment : il est plus habituel d'écrire 1 + 2 que + 1 2.

Il est possible en OCaml de définir de tels opérateurs, en utilisant les parenthèses autour de l'identifiant et en utilisant pour ce dernier certains caractères réservés :

# let (++) (x, y) n = (x + n, y + n);;
val ( ++ ) : int * int -> int -> int * int = <fun>
# (2, 0) ++ 2 (* utilisation en position infixe *) ;;
- : int * int = (4, 2)
# (++) (2, 0) 2 (* utilisation en position préfixe *) ;;
- : int * int = (4, 2)

Les caractères réservés à ce genre de définition sont indiqués ici : http://caml.inria.fr/pub/docs/manual-ocaml/lex.html.

Ordre supérieur

«Une valeur fonctionnelle (une fermeture) peut être retournée comme résultat. Elle peut également être prise comme argument d'une fonction. Les fonctions prenant en argument ou retournant des valeurs fonctionnelles sont dites d'ordre supérieur.» Par exemple :

# let h = function f -> function y -> (f y) + y ;;
val h : (int -> int) -> int -> int = <fun>

La bibliothèques standard est pleine de fonctions d'ordre supérieur, souvent polymorphes. Ainsi, la fonction List.map prend en paramètres une fonction et une liste et applique la fonction à chaque élément de la liste pour construire une nouvelle liste :

# List.map ;;
- : ('a -> 'b) -> 'a list -> 'b list = <fun>
# List.map square [ 1 ; 2 ; 3 ; 4 ; 5 ; 6 ];;
- : int list = [1; 4; 9; 16; 25; 36]

2.1.6  Récursion

L'ajout à l'environnement d'une variable globale ou locale est fait à la fin de la déclaration. Il est donc impossible d'utiliser la variable dans sa définition, puisqu'elle n'est pas encore dans l'environnement :

# let compte x = if x <= 0 then x else compte (x-1) + 1;;
File "", line 1, characters 37-43:
Error: Unbound value compte

Pour pouvoir déclarer des valeurs récursives (qui font appel à elles-mêmes), il faut explicitement les indiquer comme telles, en utilisant le mot-clé rec :

# let rec compte x = if x <= 0 then x else compte (x-1) + 1;;
val compte : int -> int = <fun>
# compte 10;;
- : int = 10

Il est également possible de définir des valeurs mutuellement récursives globales  :

# let rec triple x = if x <= 0 then 0 else triple_aux (x - 2) + 1
and triple_aux x = triple (x+1) + 2;;
val triple : int -> int = <fun>
val triple_aux : int
-> int = <fun>
# triple 20;;
- : int = 60

ou locales :

# let rec triple2 x = if x <= 0 then 0 else triple2_aux (x - 2) + 1
and triple2_aux x = triple2 (x+1) + 2 in
triple2 20;;
- : int = 60
# triple2 30;;
File "", line 1, characters 0-7:
Error: Unbound value triple2

2.1.7  Polymorphisme et contrainte de type

Polymorphisme

«Les fonctions dont l'un des paramètres ou la valeur de retour est d'un type qu'il n'est pas nécessaire de préciser sont dites polymorphes. Le synthétiseur de types contenu dans le compilateur d'OCaml trouve le type le plus général pour chaque expression. Dans ce cas, OCaml utilise des variables, ici 'a et 'b, pour désigner ces types généraux. Ces variables sont instanciées par le type de l'argument lors de l'application de la fonction.»

«Avec les fonctions polymorphes d'OCaml nous cumulons les avantages de pouvoir écrire un code générique utilisable pour des valeurs de tout type, tout en conservant la sûreté d'exécution du typage statique. En outre, la vérification des types est effectuée à la compilation, donc la généricité du code ne nuit pas à l'efficacité du programme. »

Quelques exemples :

# fst ;;
- : 'a * 'b -> 'a = <fun>
# fst (1, 'a');;
- : int = 1
# fst ("hello", 1.2);;
- : string = "hello"
# List.map ;;
- : ('a -> 'b) -> 'a list -> 'b list = <fun>
# square ;;
- : int -> int = <fun>
# List.map square [ 1 ; 2 ; 3 ; 4 ; 5 ; 6 ];;
- : int list = [1; 4; 9; 16; 25; 36]
# string_of_int ;;
- : int -> string = <fun>
# List.map string_of_int [ 1 ; 2 ; 3 ; 4 ; 5 ; 6 ];;
- : string list = ["1"; "2"; "3"; "4"; "5"; "6"]

Contrainte de type

Il est possible de forcer le type des valeurs, ce qui est utile dans plusieurs cas :

Les contraintes de types d'une variable sont notées ainsi :

( <identifiant> : <type> )

Exemple :

# let (mon_map : ('a -> int) -> 'a list -> int list) = List.map;;
val mon_map : ('a -> int) -> 'a list -> int list = <fun>
# mon_map square [ 1 ; 2 ; 3 ; 4 ; 5 ; 6 ];;
- : int list = [1; 4; 9; 16; 25; 36]
# mon_map string_of_int [ 1 ; 2 ; 3 ; 4 ; 5 ; 6 ];;
File "", line 1, characters 8-21:
Error: This expression has type int
-> string
but an expression was expected of type int -> int
# mon_map int_of_string [ "1" ; "2" ; "3" ; "4" ; "5" ; "6" ];;
- : int list = [1; 2; 3; 4; 5; 6]

Attention

Il n'est pas possible de forcer le type d'un identifiant à être polymorphe là où il ne l'est pas :

# let (f : 'a -> 'a -> int) = fun x y -> x + 1;;
val f : int -> int -> int = <fun>

2.1.8  Exercices

On pourra s'aider de la documentation du module List .

Exercice 1. Redéfinition de List.map. Réécrire la fonction List.map, prenant en paramètres une fonction f et une liste d'éléments et retournant une nouvelle liste, résultat de l'application de f à chaque élément. La fonction conserve l'ordre des éléments. On utilisera la récursion et les fonctions List.length, List.tl, List.hd et le constructeur ::.
# let rec map f l =
if List.length l <= 0 then
[]
else
(f (List.hd l)) :: (map f (List.tl l));;
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>
# map string_of_int [ 1 ; 2 ; 3 ; 4 ; 5 ; 6];;
- : string list = ["1"; "2"; "3"; "4"; "5"; "6"]

Exercice 2. Ecrire une fonction prenant une liste d'entiers et retournant le plus petit ou max_int si la liste est vide.
# let min_elt = List.fold_left min max_int;;
val min_elt : int list -> int = <fun>
# min_elt [ 10 ; 4 ; 45; 69 ; 2 ; 13 ];;
- : int = 2
# min_elt [];;
- : int = 4611686018427387903

Exercice 3. Ecrire une fonction prenant une liste d'entiers et retournant la moyenne sous forme d'un flottant, ou 0. si la liste est vide.
# let avg list =
if List.length list > 0 then
let sum = List.fold_left (+) 0 list in
(float sum) /. (float (List.length list))
else
0.0;;
val avg : int list -> float = <fun>
# avg [ 10 ; 4 ; 45; 69 ; 2 ; 13 ];;
- : float = 23.8333333333333321
# avg [];;
- : float = 0.

2.2  Déclaration de types et filtrage de motifs

2.2.1  Filtrage de motif (pattern matching)

Le filtrage permet :

Les motifs s'expriment dans une syntaxe presque identique à celle des expressions, bien qu'il ne soit pas question d'évaluation mais de test sur la structure et le contenu des valeurs filtrées. Le terme anglais pour le filtrage est pattern matching.

La syntaxe est la suivante :

match <expression0> with
|
<pattern1> -> <expression1> 
|
<pattern2> -> <expression2> 
| ...

L'évaluation de cette expression se déroule ainsi : L'expression0 est évaluée. Sa valeur est ensuite comparée au premier motif pattern1. Si la valeur est filtrée (elle correspond aux contraintes de ce motif), alors l'expression1 est évaluée en une valeur qui est la valeur de toute la construction match ... with. Si ce n'est pas le cas, la valeur de expression0 est comparée au motif pattern2, et ainsi de suite jusqu'à ce qu'un motif corresponde à la valeur.

Si aucun motif ne correspond à la valeur à filtrer, une exception (cf. section 2.3.1) Match_failure est levée. Pour se prémunir contre cette erreur de programmation qui correspond à un cas non prévu par le programmeur, deux solutions :

Les motifs permettent à la fois

Voyons maintenant quelques exemples.

# match 1 + 1 with
0 -> print_endline "bizarre"
|
1 -> print_endline "etrange"
|
2 -> print_endline "ouf!"
;;
File "", line 1, characters 0-106:
Warning 8: this pattern
-matching is not exhaustive.
Here is an example of a value that is not matched:
3
ouf!
- : unit = ()

Dans l'exemple ci-dessus, l'expression 1 + 1 est évaluée et ensuite comparée jusqu'à trouver un motif lui correspondant, ici 2, ce qui provoque l'évaluation de l'expression print_endline "ouf!". Le compilateur émet un avertissement indiquant que la construction de filtrage n'est pas exhaustive, en exhibant un cas non filtré, la valeur 3.

# match List.map string_of_int [ 1 ; 2 ; 3 ] with
[]
| _ :: [] -> assert false
| [s1 ; s2] -> assert false
| s1 :: s2 :: s3 :: _ ->
List.iter print_endline [ s1 ; s2 ; s3 ]
| l ->
List.iter print_endline l
;;
File "", line 7, characters 2-3:
Warning 11: this match case is unused.
1
2
3
- : unit = ()

Dans cet exemple, nous filtrons une valeur de type string list. Les premier ([]) et deuxième (_::[]) motifs filtrent respectivement la liste vide et n'importe quelle liste à un élément, et partagent le même traitement associé (assert1 false). Le troisième motif [s1;s2] filtre les listes à deux éléments, en liant le premier élément à l'identifiant s1 et le second à l'élément s2. Dans notre exemple, la liste filtrée aura toujours trois éléments donc nous ne passerons jamais dans cette branche.
Le motif s1 :: s2 :: s3 :: _ filtre toutes les listes d'au moins trois éléments. Il utilise l'opérateur de construction de liste :: (appelé cons), avec à gauche un élément et à droite une liste. Dans ce motif, les trois premiers éléments de la liste sont associés aux identifiants s1, s2 et s3. Les identifiants créés dans un motif ne sont visibles que dans l'expression associée à ce motif.
Enfin, OCaml nous indique que le dernier motif l, qui permet de filtrer n'importe quelle liste en l'associant à l'identifiant l, est inutile car les motifs précédents permettent de filtrer tous les cas possibles.

Une erreur fréquente lorsqu'on commence à utiliser la structure de filtrage est d'utiliser un identifiant pour imposer une contrainte d'égalité avec un identifiant prédéfini :

# let x = 3;;
val x : int = 3
# match [ 0 ] with
[x] -> true
| _ -> false
;;
- : bool = true

Ce n'est pas l'effet attendu. Le x du motif [x] crée une nouvelle liaison dans l'environnement entre un nouvel identifiant x et la valeur 0. Il n'est fait aucune comparaison avec l'identifiant x précédemment défini. La bonne façon de faire sera d'utiliser une clause when, permettant d'ajouter une expression de type bool à un motif. Dans ce cas, l'expression associée au motif (à droite de ->) ne sera exécutée que si l'expression est évaluée à vraie. Si ce n'est pas le cas, l'effet sera le même que si le motif ne correspondait pas à la valeur filtrée, et l'évaluation de la structure de filtrage passera au motif suivant :

# match [ 0 ] with
| [y] when y = x -> true
| _ -> false
;;
- : bool = false

A noter que l'on n'écrira pas :

match [ 0 ] with
| [x] when x = x -> true
| _ -> false
;;

car les deux x de x = x font référence tous deux à l'identifiant créé dans le motif [x]. Les clauses when sont également appelées gardes.

Attention

Il n'est pas possible de lier plusieurs fois la même variable dans un motif, y compris pour exprimer une contrainte d'égalité entre deux parties de l'expression filtrée :

# match (1,2) with
(x,x) -> true
| _ -> false
;;
File "", line 2, characters 5-6:
Error: Variable x is bound several times in this matching

Le mot clé as permet de créer un nouvel identifiant pour tout ou partie d'un motif :

# match [ [ 1 ; 2 ] ; [ 3 ; 4 ] ] with
[ [ 1 ; 2 ] as liste1 ; liste2 ] as tout ->
List.iter print_int liste1;
print_newline();
List.iter (fun l -> List.iter print_int l) tout;
print_newline();
| _ -> assert false
;;
12
1234
- : unit = ()

Nous verrons d'autres exemples de filtrage dans la suite du document, car cette construction est très utilisée.

Exercice

Exercice 4. Définir une fonction prenant en paramètre une liste d'éléments quelconques et retournant vrai si la liste est un palindrome, faux sinon.
# let palindrome_list l =
let len = List.length l in
let rec iter n = function
[], [] -> true
| h1::q1, h2::q2 ->
(n > len / 2) || ( (h1 = h2) && iter (n + 1) (q1, q2) )
| _ -> assert false (* les listes en parametre ont la meme longueur *)
in
iter 0 (l, List.rev l)
;;
val palindrome_list : 'a list -> bool = <fun>

Quelques tests:

# palindrome_list [1 ; 2 ; 3 ; 2 ; 1 ];;
- : bool = true
# palindrome_list ['1' ; '2' ; '3' ; '3' ; '2' ; '1' ];;
- : bool = true
# palindrome_list ['r' ; 'a' ; 'd' ; 'a'; 'r'];;
- : bool = true
# palindrome_list ['r' ; 'a' ; 'v' ; 'e'; 'r'];;
- : bool = false
# palindrome_list [1.0 ; 2.0 ; 3.0 ; 4.0 ; 2.0 ; 1.0];;
- : bool = false
# palindrome_list [];;
- : bool = true

2.2.2  Déclaration de valeurs par filtrage d'un motif

Il est possible d'utiliser le pattern-matching pour déclarer plusieurs variables en même temps, en filtrant le résultat d'une expression :

# let (x, y, z) = (1, "2", 3.0);;
val x : int = 1
val y : string = "2"
val z : float = 3.

# let [s1 ; s2 ; s3] = [ "un" ; "deux" ; "trois"];;
File "", line 1, characters 4-18:
Warning 8: this pattern
-matching is not exhaustive.
Here is an example of a value that is not matched:
[]
val s1 : string = "un"
val s2 : string = "deux"
val s3 : string = "trois"

# let [x1 ; x2] = [ 1 ; 2 ; 3 ] ;;
File "", line 1, characters 4-13:
Warning 8: this pattern
-matching is not exhaustive.
Here is an example of a value that is not matched:
[]
Exception: Match_failure ("", 1, 4).

2.2.3  Déclaration de types

OCaml permet de définir ses propres types de données, en plus de ceux de base. La syntaxe est la suivante :

type <identifiant> = <définition>

Les types mutuellement récursifs seront définis de la façon suivante :

type <identifiant1> = <définition1>
and
<identifiant2> = <définition2>
...
and
<identifiantN> = <définitionN>

On remarquera qu'il n'y a pas de mot-clé rec pour les définitions de type, chaque type pouvant être récursif sans syntaxe particulière.

Les identifiants de types ont la même syntaxe que les identifiants de valeurs, mais sont dans un espace de noms différent, ce qui signifie que l'on peut définir une valeur foo et un type foo sans problème.

Les paramètres des types paramétrés doivent être indiqués avant l'identifiant :

type 'a <identifiant> = <définition utilisant le paramètre de type 'a>
type ('a, 'b)
<identifiant> = <définition utilisant 'a et 'b>

Ainsi, on peut définir le type des listes de paires d'éléments d'un type en paramètre :

# type 'a pairs = ('a * 'a) list;;
type 'a pairs = ('a * 'a) list
# let int_pair_list = [ (1, 2) ; (5, 4) ] ;;
val int_pair_list : (int * int) list = [(1, 2); (5, 4)]
# let (int_pairs : int pairs) = [ (1, 2) ; (3, -3) ; (6, 8)];;
val int_pairs : int pairs = [(1, 2); (3, -3); (6, 8)]
# let (bool_pairs : bool pairs) = [ (true, true) ; (false, false) ; (true, false)];;
val bool_pairs : bool pairs = [(true, true); (false, false); (true, false)]

Ce type étant défini uniquement à partir de types existants, nous devons indiquer explicitement le type que nous voulons voir apparaître, sinon le compilateur infère le type à partir des types de base, comme pour int_pair_list ci-dessus. Cependant, nous pouvons utiliser indifféremment des valeurs de type 'a pairs ou du type ('a * 'a) list, le compilateur réalisant l'unification de ces types pour vérifier qu'ils sont bien compatibles :

# (=);;
- : 'a -> 'a -> bool = <fun>
# int_pair_list;;
- : (int * int) list = [(1, 2); (5, 4)]
# int_pairs;;
- : int pairs = [(1, 2); (3, -3); (6, 8)]
# int_pair_list = int_pairs;;
- : bool = false

Si les types ne sont pas compatibles, le compilateur signale une erreur :

# let (float_pairs : float pairs) = [ (1., 2.) ; (3., 4.)];;
val float_pairs : float pairs = [(1., 2.); (3., 4.)]
# int_pairs = float_pairs;;
File "", line 1, characters 12-23:
Error: This expression has type float pairs = (float
* float) list
but an expression was expected of type int pairs = (int * int) list

2.2.4  Enregistrements

Définition d'un type

«Les enregistrements sont des n-uplets dont chaque champ est nommé à la manière des record de Pascal ou des struct de C. Un enregistrement correspond toujours à la déclaration d'un nouveau type. Un type enregistrement est défini par la déclaration de son nom, du nom de chacun de ses champs et de leur type.»

La syntaxe est la suivante :

type [<variables de type>] <identifiant> = {
<nom_de_champ1> : <type1> ;
...
<nom_de_champn> : <typen> ;
}

Un type enregistrement ne peut avoir deux noms de champs identiques.

Création de valeurs

Par mesure de sûreté, le compilateur impose que chaque champ soit initialisé, lors de la création d'une valeur de type enregistrement. Cela évite des problèmes de champs non initialisés comme on peut en rencontrer en langage C.

On peut créer une valeur d'un type enregistrement de deux façons :

L'ordre dans lequel le contenu des champs est défini est sans importance. Cependant, on aura à cœur de définir les champs toujours dans le même ordre afin de faciliter la lecture et la maintenance du programme. Par ailleurs, l'ordre d'évaluation lors de l'affectation des champs n'étant pas spécifié, on ne se reposera pas dessus si celles-ci ont des effets de bord (lors de l'utilisation de traits impératifs du langage).

Accès aux champs

L'accès à un champ d'une valeur de type enregistrement se fait en utilisant le point '.' entre l'identifiant de la valeur et le nom du champ. Ainsi, foo.bar.gee représente l'accès à la valeur du champ gee de la valeur du champ bar de la valeur foo.

Filtrage

Il est bien sûr possible d'utiliser le filtrage sur des valeurs de type enregistrement, en mettant des contraintes sur tout ou partie des champs du type :

match <expression> with
{ champ
i = <motifi> ; ... ; champy = <motify> } -> ...

Exemple

Voici un exemple de définition et d'utilisation d'un type enregistrement coord avec deux champs x et y, tous deux de type float :

# type coord = { x : float ; y : float};;
type coord = { x : float; y : float; }
# let origin = { x = 0. ; y = 0. };;
val origin : coord = {x = 0.; y = 0.}
# let add p1 p2 = { x = p1.x +. p2.x ; y = p1.y +. p2.y };;
val add : coord -> coord -> coord = <fun>
# let unite = { x = 1. ; y = 1. };;
val unite : coord = {x = 1.; y = 1.}
# let unite_x = { unite with y = 0. };;
val unite_x : coord = {x = 1.; y = 0.}
# let p1 = add (add origin unite) unite;;
val p1 : coord = {x = 2.; y = 2.}
# p1 = unite;;
- : bool = false
# match add p1 unite with
{ x = mon_x } as foo when foo.y > 1. -> string_of_float mon_x
|
{ x = x ; y = y } -> string_of_float (x +. y);;
- : string = "3."

2.2.5  Types somme

Définition d'un type

«À la différence des n-uplets ou des enregistrements, qui correspondent à un produit cartésien, la déclaration d'un type somme correspond à une union d'ensembles. On regroupe dans un même type des types différents (par exemple des entiers et des chaînes de caractères). Les différents membres de la somme sont discriminés par des constructeurs qui permettent d'une part, comme leur nom l'indique, de construire les valeurs de ce type et d'autre part d'accéder aux composantes de ces valeurs grâce au filtrage de motif. Appliquer un constructeur à un argument, c'est indiquer que la valeur retournée appartient à ce nouveau type.»

«On déclare un type somme en donnant le nom de ses constructeurs et le type de leur éventuel argument.»

La syntaxe est la suivante :

type [<variables de type>] <identifiant> =
|
<Constructeur1>
...
|
<Constructeuri> of <typei>
...
|
<Constructeurn> of <typen1> * ... * <typenm>

Les noms des constructeurs doivent commencer par une majuscule.

Chaque constructeur d'un type somme peut avoir 0, 1 ou plusieurs paramètres, dont le type est indiqué dans la définition du type somme.

Un type somme ne peut avoir deux constructeurs de même nom.

Voici un exemple de définition d'un type somme :

# type direction =
Nord
| Sud
| Est
| Ouest
| Compose of direction * direction;;
type direction = Nord | Sud | Est | Ouest | Compose of direction * direction

Création de valeurs

Une valeur d'un type somme est créée à l'aide d'un constructeur de ce type et des paramètres adaptés selon le constructeur, par la syntaxe suivante :

Avec notre exemple de type direction, cela donne par exemple :

# let nne = Compose (Nord, Compose (Nord, Est));;
val nne : direction = Compose (Nord, Compose (Nord, Est))

Filtrage

Il est bien sûr possible d'utiliser le filtrage sur des valeurs de type somme, en procédant par cas sur les constructeurs du type :

match <expression de type somme> with
|
<Constructeur1> -> <expression1>
|
<Constructeuri> <motifi> -> <expressioni>
|
<Constructeurj> (<motifj1>, ..., <motifjn>) -> <expressionj>
...

Par exemple:

# let rec string_of_direction = function
Nord -> "nord"
|
Sud -> "sud"
|
Est -> "est"
|
Ouest -> "ouest"
|
Compose (d1, d2) -> (string_of_direction d1)^"-"^(string_of_direction d2);;
val string_of_direction : direction -> string = <fun>
# string_of_direction nne;;
- : string = "nord-nord-est"

Exercice

Exercice 5. Définir un type somme expr pour représenter les expressions contenant les 4 opérations habituelles sur les entiers ainsi que les valeurs entières. On aura donc 5 constructeurs.
type expr =
Val of int
| Add of expr * expr
|
Sub of expr * expr
|
Mul of expr * expr
|
Div of expr * expr;;

Ensuite, définir une fonction prenant une valeur de ce type et calculant la valeur entière en réalisant l'opération décrite.

# let rec eval = function
Val n -> n
|
Add (e1, e2) -> (eval e1) + (eval e2)
| Sub (e1, e2) -> (eval e1) - (eval e2)
| Mul (e1, e2) -> (eval e1) * (eval e2)
| Div (e1, e2) -> (eval e1) / (eval e2)
;;
val eval : expr -> int = <fun>
# eval (Add (Val 2, Mul (Val 4, Sub (Val 12, Val 2))));;
- : int = 42

2.2.6  Portée des déclarations

Les déclarations de type ont la même portée que les déclarations globales de variables et peuvent comme elles être masquées par de nouvelles déclarations avec des noms similaires. Cependant, les valeurs définies avec un type t ne changent pas de type en cas de définition d'un nouveau type t (et heureusement!). Par contre, ces homonymies peuvent rendre les messages d'erreur du compilateur un peu déroutants :

# type t = Foo | Bar;;
type t = Foo | Bar
# let x = Foo;;
val x : t = Foo
# type t = Gee | Buz;;
type t = Gee | Buz
# let y = Gee;;
val y : t = Gee
# x = y;;
File "", line 1, characters 4-5:
Error: This expression has type t/1266 but an expression was expected of type
t/1260

Le même phénomène de masquage est possible sur les noms des champs des types enregistrements :

# type rec1 = { champ : int; champ2: int };;
type rec1 = { champ : int; champ2 : int; }
# let x = { champ = 0 ; champ2 = 1;};;
val x : rec1 = {champ = 0; champ2 = 1}
# type rec2 = { champ : int ; champ3 : int};;
type rec2 = { champ : int; champ3 : int; }
# let y = { x with champ = 3 };;
File "", line 1, characters 10-11:
Error: This expression has type rec1 but an expression was expected of type
rec2

et sur les noms des constructeurs de types sommes :

# type t = Foo | Bar;;
type t = Foo | Bar
# let x = Foo;;
val x : t = Foo
# type t2 = Foo;;
type t2 = Foo
# match x with
Foo -> 0
| Bar -> 1;;
File "", line 3, characters 2-5:
Error: This pattern matches values of type t
but a pattern was expected which matches values of type t2
# match x with
| Foo -> 0;;
File "", line 2, characters 2-5:
Error: This pattern matches values of type t2
but a pattern was expected which matches values of type t

En conséquence, il est souvent utile de choisir des noms "à préfixe" pour les champs et les constructeurs, par exemple :

type user = { user_name : string; user_login: string};;

type host = { host_name : string; host_ipv4: int * int * int * int};;

Même si cela est plus verbeux, cela facilite la lecture et la maintenance des programmes.

2.2.7  Types fonctionnels

Les types fonctionnels sont les types représentant des fonctions, comme nous en avons déjà vu précédemment. Il est possible de créer ses propres types fonctionnels ou encore d'utiliser des types fonctionnels pour les champs d'enregistrements ou les paramètres de constructeurs.

Ainsi, on peut définir le type d'un afficheur de messages :

# type printer = string -> unit;;
type printer = string -> unit

Tout comme il est possible d'avoir des champs fonctionnels dans un type enregistrement :

type output_context = {
output_error : string -> unit ;
output_message : string -> unit ;
};;

ou bien, en utilisant notre type printer :

type output_context = {
output_error : printer ;
output_message : printer ;
};;

On peut également avoir des paramètres fonctionnels pour des constructeurs :

type action =
Init of (unit -> output_context)
| Compute of (unit -> unit)
| Terminate of (unit -> unit);;

Ces valeurs fonctionnelles peuvent être nommées dans des motifs de filtrage pour être appliquées :

# let action = Compute (fun () -> print_endline "coucou");;
val action : action = Compute <fun>
# match action with
Compute f -> f () (* application de la fonction f a () *)
|
Init f -> ignore(f())
| Terminate f -> f();;
coucou
- : unit = ()

2.2.8  Exercice

Exercice 6. On souhaite rendre les expressions de l'exercice 5 génériques. Pour cela, ajouter un paramètre de type au type expr pour ne plus limiter les valeurs des expressions à des entiers.
type 'a expr =
Val of 'a
|
Add of 'a expr * 'a expr
|
Sub of 'a expr * 'a expr
|
Mul of 'a expr * 'a expr
|
Div of 'a expr * 'a expr;;

Ensuite, on veut pouvoir paramétrer la fonction eval pour spécifier le traitement à effectuer pour chaque opérateur de notre type expression. Définir un type enregistrement operators permettant de donner la fonction associée à chaque opérateur d'expression. Ce type est paramétré par le type des valeurs calculées.

type 'a operators = {
op_add : 'a -> 'a -> 'a ;
op_sub : 'a -> 'a -> 'a ;
op_mul : 'a -> 'a -> 'a ;
op_div : 'a -> 'a -> 'a ;
};;

Enfin, définir une nouvelle fonction eval prenant en paramètre supplémentaire les fonctions à utiliser pour effectuer les calculs lors de l'évaluation (une valeur de type operators).

# let rec eval ops = function
Val v -> v
|
Add (e1, e2) -> ops.op_add (eval ops e1) (eval ops e2)
| Sub (e1, e2) -> ops.op_sub (eval ops e1) (eval ops e2)
| Mul (e1, e2) -> ops.op_mul (eval ops e1) (eval ops e2)
| Div (e1, e2) -> ops.op_div (eval ops e1) (eval ops e2)
;;
val eval : 'a operators -> 'a expr -> 'a = <fun>

Définir une valeur du type operators et s'en servir pour évaluer une expression contenant des flottants.

# let float_ops = {
op_add = (+.) ;
op_sub = (-.) ;
op_mul = ( *. ) ;
op_div = (/.) ;
};;
val float_ops : float operators =
{op_add = <fun>; op_sub = <fun>; op_mul = <fun>; op_div = <fun>}
# eval float_ops (Add (Val 2., Mul (Val 4., Sub (Val 12., Val 2.))));;
- : float = 42.

2.3  Typage, domaine de définition et exceptions

«Le type inféré d'une fonction correspond à un sur-ensemble de son domaine de définition. Ce n'est pas parce qu'une fonction prend un paramètre de type int qu'elle saura calculer une valeur pour tous les entiers passés en argument. On traite en général ce problème en utilisant le mécanisme d'exceptions d'OCaml. Le déclenchement d'une exception provoque une rupture du calcul qui peut être interceptée et traitée par le programme. Pour cela l'exécution du programme doit avoir posé un récupérateur d'exception avant le calcul de l'expression qui provoque le déclenchement de cette exception.»

2.3.1  Fonctions partielles et exceptions

«Le domaine de définition d'une fonction correspond à l'ensemble des valeurs sur lesquelles la fonction effectue son calcul.»

Que faire quand est passée à une fonction une valeur qu'elle ne sait pas traiter ? Retourner une valeur arbitraire n'est pas la bonne solution. Les exceptions sont faites pour gérer ces situations. Par exemple, la fonction List.hd récupérant le premier élément d'une liste lèvera une exception Invalid_argument si une liste vide lui est passée.

Une fonction peut être partielle parce qu'elle ne traite pas tous les formes possibles d'un paramètre :

# type dummy = One | Two | Three;;
type dummy = One | Two | Three
# let succ = function
One -> Two
| Two -> Three
;;
File "", line 1, characters 11-47:
Warning 8: this pattern
-matching is not exhaustive.
Here is an example of a value that is not matched:
Three
val succ : dummy
-> dummy = <fun>

Le compilateur signale que la fonction est partielle, en indiquant que la structure de filtrage ne traite pas le cas Three. En effet, l'application de cette fonction à la valeur Three déclenchera une exception prédéfinie pour ce genre de cas :

# succ Three;;
Exception: Match_failure ("", 1, 11).

Ce genre d'exception n'est pas très facile à traiter car elle est utilisée pour n'importe quel pattern-matching non exhaustif. On préférera donc définir et utiliser ses propres exceptions afin d'être en mesure de traiter efficacement et précisément chaque erreur.

Les exceptions prédéfinies et utilisées par les modules de la bibliothèque standard d'OCaml sont indiquées ici : http://caml.inria.fr/pub/docs/manual-ocaml/manual033.html#htoc253

2.3.2  Définition d'une exception

Une nouvelle exception est définie avec la syntaxe

exception <Identifiant>

L'identifiant doit commencer par une majuscule, comme pour les constructeurs de type somme. On peut également définir une exception portant une valeur d'un type indiqué à la définition :

exception <Identifiant> of <type>

ce qui permet de passer de l'information sur les causes, l'endroit et les conditions menant à ce comportement exceptionnel.

# exception Mon_erreur of string;;
exception Mon_erreur of string

Les exceptions peuvent être vues comme des valeurs d'un type somme exn qui serait enrichi à chaque création d'exception :

# Mon_erreur "message";;
- : exn = Mon_erreur "message"

Remarque: Le type d'une exception ne peut être polymorphe.

2.3.3  Déclenchement d'une exception

Une exception est déclenchée (ou levée) grâce à la fonction raise prédéfinie :

# raise (Mon_erreur "Ceci n'est pas un message d'erreur");;
Exception: Mon_erreur "Ceci n'est pas un message d'erreur".

Nous pouvons redéfinir notre fonction succ pour lever une exception appropriée :

# let succ = function
One -> Two
| Two -> Three
| Three -> raise (Mon_erreur "Je ne sais pas compter plus loin")
;;
val succ : dummy -> dummy = <fun>

La levée d'une exception interrompt l'évaluation normale de l'expression qui la contient. Cela peut avoir de l'importance lorsque l'ordre d'évaluation n'est pas spécifié, comme dans l'exemple suivant :

# let f x =
(* l'affichage, trait imperatif par excellence,
n'est la que pour montrer l'ordre d'execution *)
print_endline (Printf.sprintf "Appel de f avec x = %d" x);
if x < 0 then
raise (Mon_erreur "c'est negatif !")
else
x + 1;;
val f : int -> int = <fun>
# (f (-1)) + (f 1);;
Appel de f avec x = 1
Appel de f avec x =
-1
Exception: Mon_erreur "c'est negatif !".

On constate ici que l'ordre d'évaluation des arguments passés à une fonction (ici l'opérateur infixe (+)) est de droite à gauche. Cela signifie que nous avons tout de même effectué une partie du calcul avant de lever l'exception. De telles situations peuvent rendre plus difficile la correction d'un programme. On s'appliquera parfois, pour se prémunir contre de telles situations, à forcer l'ordre d'évaluation des paramètres avant de les passer à la fonction :

# let p1 = f (-1) in
let p2 = f 1 in
p1 + p2;;
Appel de f avec x = -1
Exception: Mon_erreur "c'est negatif !".

On se trouve ici au bord de la programmation impérative.

2.3.4  Récupération d'une exception

La construction suivante permet de récupérer (on dit aussi "attraper") une exception levée dans l'<expression> :

try <expression>
with
<motif1> -> <expression1>
| ...
|
<motifn> -> <expressionn>

Dans la partie with, on donne des motifs de filtrage d'exception, qui fonctionnent de la même façon que les motifs de filtrage de valeurs. Concrètement, on pourra écrire comme suite de notre exemple :

# try ignore(succ Three)
with
Mon_erreur s -> print_endline ("Erreur: "^s)
| Failure s -> print_endline ("Failure: "^s);;
Erreur: Je ne sais pas compter plus loin
- : unit = ()

Contrairement au filtrage sur des valeurs, le compilateur ne peut ici indiquer si les motifs du bloc with sont exhaustifs. Cela nécessiterait de connaître toutes les exceptions potentiellement levées à chaque point du code, ce qui demanderait une analyse de flots de données, notamment à cause des possibilités de passage de fonction en paramètre. En effet, une telle fonction pourrait lever une exception, mais, dans bien des situations, le paramètre effectif ne sera connu qu'à l'exécution.

Par ailleurs, on ne souhaite pas forcément rattraper toutes les exceptions possibles à chaque point d'un programme, mais plutôt rattraper seulement certaines, que l'on peut traiter, en laissant volontairement remonter celles correspondant à des cas plus inattendus dans lequels on ne peut rien faire d'autre qu'interrompre le traitement en cours.

Il est possible de rattraper toutes les exceptions, avec le motif universel '_', ou bien de lier l'exception rattrapée à un identifiant, comme ci-dessous :

# try List.hd []
with
e ->
(* traitement effectue pour n'importe quelle exception *)
print_endline "An exception was caught!";
let msg =
match e with
Failure s | Sys_error s -> s
| _ -> Printexc.to_string e
in
print_endline ("Some info about the exception: "^msg);
(* relancons l'exception *)
raise e;;
An exception was caught!
Some info about the exception: hd
Exception: Failure "hd".

2.3.5  Exercice

Exercice 7. Ecrire une fonction try_finalize spécifiée de la façon suivante : try_finalize f x g y applique la fonction f à la valeur x.

  • si une exception est levée, alors on évalue g y et on relève ensuite l'exception,
  • si aucune exception n'est levée, on évalue aussi g y et on retourne la valeur retournée par f x.
  • si une exception est levée par g y, cette exception est levée par la fonction try_finalize.

# let try_finalize f x g y =
let v = try f x with exn -> ignore(g y); raise exn in
ignore(g y);
v;;
val try_finalize : ('a -> 'b) -> 'a -> ('c -> 'd) -> 'c -> 'b = <fun>
# try_finalize (fun x -> x * 2) 21 print_endline "application de g a y";;
application de g a y
- : int = 42
# try_finalize (fun x -> 1 / x) 0 print_endline "application de g a y";;
application de g a y
Exception: Division_by_zero.

2.4  Récursivité terminale

La programmation fonctionnelle conduit souvent à écrire des fonctions récursives. Il existe deux types de récursivités : terminale et non-terminale. Illustrons cette différence sur un exemple, la fonction de map sur les listes.

Voici une première version, récursive non-terminale :

# let rec map_nt f = function
[] -> []
| h::q -> (f h) :: (map_nt f q);;
val map_nt : ('a -> 'b) -> 'a list -> 'b list = <fun>
# map_nt ((+) 1) [ 1 ; 2 ; 3 ; 4 ];;
- : int list = [2; 3; 4; 5]

La fonction map_nt effectue bien le calcul demandé, mais présente un risque de débordement de pile dans le cas de grandes listes :

# let big_list =
let rec build n acc =
if n < 2_000_000 then build (n+1) (n::acc) else acc
in
build 0 [];;
val big_list : int list =
[1999999; 1999998; 1999997; 1999996; 1999995; 1999994; 1999993; 1999992;
1999991; 1999990; 1999989; 1999988; 1999987; 1999986; 1999985; 1999984;
1999983; 1999982; 1999981; ...]
# map_nt ((+) 1) big_list;;
Stack overflow during evaluation (looping recursion?).

Que se passe-t-il ? Notre fonction map_nt fonctionne par cas sur la structure de la liste en paramètre :

Voyons maintenant une autre définition, cette fois récursive terminale :

# let map f l =
let rec iter acc = function
[] -> List.rev acc
| h::q -> iter ((f h) :: acc) q
in
iter [] l;;
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>
# map string_of_int big_list;;
- : string list =
["1999999"; "1999998"; "1999997"; "1999996"; "1999995"; "1999994"; "1999993";
"1999992"; "1999991"; "1999990"; "1999989"; "1999988"; "1999987"; "1999986";
"1999985"; "1999984"; "1999983"; "1999982"; "1999981"; ...
]

Notre fonction map n'est plus récursive mais se contente de définir et d'appeler une autre fonction iter qui, elle, est récursive terminale. Cette fonction iter prend en paramètre un accumulateur, ce qui permet, lors de l'appel récursif, d'avoir déjà effectué la création de la nouvelle liste composée de la nouvelle tête (f h) et du reste de la nouvelle liste déjà calculé (acc).

Cette façon de procéder donne une liste en ordre inverse, d'où l'appel à List.rev pour retourner l'accumulateur (la nouvelle liste) lors qu'on tombe sur le cas de la liste vide (fin de la liste à traiter).

La fonction iter est bien récursive terminale car l'appel récursif à elle-même est bien le dernier calcul effectué dans le cas où la liste n'est pas vide.

Cependant, c'est l'appel récursif et non la fonction qui est ou non terminal. Dans l'exemple suivant

# let rec foo x =
if x < 1 then f (x + 1) else 1 + f (x - 1);;
val foo : int -> int = <fun>

la fonction foo est récursive et s'appelle à deux endroits. Le premier appel récursif (f (x + 1)) est bien terminal. Par contre, le second (f (x - 1)) ne l'est pas, car la valeur de retour de cet appel sera additionnée à 1. On ne peut donc pas parler de fonction récursive terminale mais d'un appel terminal et d'un appel non terminal.

Les appels récursifs terminaux sont compilés de façon efficace (avec des sauts plutôt qu'en utilisant la pile) par le compilateur : au lieu de faire augmenter la taille de la pile, l'appel récursif est placé au même endroit que l'appel original, ce qui donne un gain de temps pour retourner la valeur finale (pas de besoin de dépiler des milliers d'appels récursifs) et un gain de place (la pile n'augmente pas de taille). Nous avons donc pu appliquer notre fonction sur big_list.

Attention

L'utilisation de constructions try ... with pour la gestion des exceptions peut faire d'une fonction récursive terminale une fonction récursive non-terminale. Supposons que nous voulions modifier notre fonction map pour, en cas d'exception Failure lors de l'application de f à un élement de la liste, conserver la valeur initiale de l'élément. Nous pouvons tenter la définition suivante :

# let safe_map f l =
let rec iter acc = function
[] -> List.rev acc
| h::q ->
try iter ((f h) :: acc) q
with Failure _ -> iter (h :: acc) q
in
iter [] l
;;
val safe_map : ('a -> 'a) -> 'a list -> 'a list = <fun>
# safe_map ((+) 1) big_list;;
Stack overflow during evaluation (looping recursion?).

La présence du bloc try ... with en vue de rattraper l'exception Failure ne permet plus au compilateur d'optimiser les appels récursifs sur la pile. En effet, l'appel récursif n'est plus forcément le dernier calcul effectué, car s'il lève une exception, il faudra la filtrer, dans ce contexte et non dans celui l'appel récursif. On écrira donc plutôt :

# let safe_map f l =
let rec iter acc = function
[] -> List.rev acc
| h::q ->
let v = try f h with Failure _ -> h in
iter (v :: acc) q
in
iter [] l
;;
val safe_map : ('a -> 'a) -> 'a list -> 'a list = <fun>
# safe_map ((+) 1) big_list;;
- : int list =
[2000000; 1999999; 1999998; 1999997; 1999996; 1999995; 1999994; 1999993;
1999992; 1999991; 1999990; 1999989; 1999988; 1999987; 1999986; 1999985;
1999984; 1999983; 1999982; ...
]

Dans ce cas, l'appel récursif redevient le dernier calcul, le rattrapage d'exception étant limité au calcul de f h.

Un appel peut être terminal même s'il ne s'agit pas d'un appel récursif. Le compilateur pourra faire des optimisations dans ce cas aussi.

L'option -annot des compilateurs OCaml génère, à la compilation d'un fichier d'implémentation foo.ml, un fichier foo.annot contenant diverses informations, notamment les types inférés pour chaque expression et, depuis la version 3.11.0, une indication de terminalité pour chaque appel de fonction : tail indiquera que l'appel est terminal, tandis que stack indiquera que l'appel se fera sur la pile.

2.5  Intermède : la recherche autour du typage


1
la fonction assert prend en paramètre une expression de type bool et lève une exception Assertion_failed si l'expression est évaluée à false.

Chapitre 3  Programmation impérative

«À la différence de la programmation fonctionnelle où on calcule une valeur par l'application d'une fonction à ses arguments sans se soucier du déroulement des opérations, la programmation impérative est plus proche de la représentation machine car elle introduit un état mémoire que le déroulement des actions d'un programme va modifier. On appelle instructions ces actions des programmes et un programme impératif est une suite, ou une séquence, d'instructions. L'état mémoire est susceptible d'être modifié à l'exécution de chaque instruction. On considère les opérations d'entrées-sorties comme des modifications de la mémoire, de la mémoire vidéo ou de fichiers.»

«Ce style de programmation est directement inspiré de la programmation assembleur. On le retrouve dans les premiers langages évolués généralistes (Fortran, C, Pascal, etc. ). En OCaml, les éléments suivants du langage correspondent à ce modèle :»

«Certains algorithmes s'écrivent plus facilement dans ce style de programmation. On peut citer comme exemple le produit de deux matrices. Même s'il est effectivement possible de le traduire dans une version purement fonctionnelle, où des listes remplacent les vecteurs, cela n'est ni naturel, ni efficace par rapport à une écriture impérative.»

«L'intérêt d'intégrer ce modèle dans un langage fonctionnel est de pouvoir écrire certains algorithmes dans ce style de programmation quand ceux-ci s'y prêtent. Les deux principaux désavantages par rapport au style purement fonctionnel sont :»

«Néanmoins, avec quelques règles de prudence dans l'écriture des programmes, le choix entre plusieurs styles de programmation offre de plus grandes possibilités d'écriture d'algorithmes, ce qui est l'objectif principal des langages de programmation. En outre, un programme écrit dans un style proche de l'algorithme utilisé sera plus simple donc aura plus de chances d'être correct (ou, tout du moins, plus rapidement mis au point).»

«Pour ces raisons, le langage OCaml possède des types de données dont les valeurs sont physiquement modifiables, des structures de contrôle de l'exécution des programmes et une bibliothèque d'entrées-sorties dans un style impératif.»

3.1  Structures de données physiquement modifiables

La programmation impérative nécessite des structures de données modifiables. OCaml en est pourvu et nous les détaillons ci-dessous.

3.1.1  Vecteurs

Les vecteurs sont des tableaux à une dimension, de taille fixe, et contenant des éléments qui sont tous du même type. Ils sont le pendant impératif des listes. Le type OCaml pour les représenter est le type paramétré 'a array, le paramètre de type 'a permettant d'avoir des vecteurs de différents types, de la même manière que le type 'a list.

Construction de valeurs

La construction d'un vecteur se fait en listant, séparées par des ';', les valeurs du vecteur entre [| et |] :

# let tab = [| 1 ; 2 ; 3 ; 4 |];;
val tab : int array = [|1; 2; 3; 4|]

Il est également possible d'utiliser des fonctions du module Array pour créer des vecteurs :

# let tab = Array.create 10 0;;
val tab : int array = [|0; 0; 0; 0; 0; 0; 0; 0; 0; 0|]
# let tab = Array.init 10 (fun i -> i * 2);;
val tab : int array = [|0; 2; 4; 6; 8; 10; 12; 14; 16; 18|]

Les indices d'un vecteur de longueur n vont de 0 à n−1.

Accès

On peut accéder de deux façons équivalentes à un élément d'un vecteur :

L'accès à un élément d'un vecteur de longueur n en utilisant un indice en dehors de la plage 0..n−1 déclenche la levée d'une exception Invalid_argument :

# tab.(42);;
Exception: Invalid_argument "index out of bounds".

On peut bien sûr également utiliser le filtrage sur les vecteurs :

# match Array.sub tab 0 2 with
[| _ ; x |] when x > 2 -> false
| [| 0 ; 2 |] -> true
| _ -> false;;
- : bool = true

Modification

Il existe deux manières équivalentes de modifier un élément d'un vecteur :

Comme pour l'accès, et en général toutes les fonctions agissant sur les vecteurs, une exception Invalid_argument est levée lors de l'utilisation d'un indice invalide (en dehors des bornes).

Autres opérations

D'autres opérations sur les vecteurs sont disponibles via des fonctions prédéfinies du module Array  :

# Array.length tab (* longueur *) ;;
- : int = 10
# Array.sub tab 3 4 (* extraction d'un sous-vecteur de longueur 4 a partir de l'indice 3 *);;
- : int array = [|6; 8; 10; 12|]
# Array.to_list tab (* creation d'une liste a partir des elements du vecteur *);;
- : int list = [42; 42; 4; 6; 8; 10; 12; 14; 16; 18]
# Array.map string_of_int tab (* map sur les elements du vecteur *) ;;
- : string array =
[|"42"; "42"; "4"; "6"; "8"; "10"; "12"; "14"; "16"; "18"|]

On consultera la documentation du module Array .

3.1.2  Chaînes de caractères

Les chaînes de caractères sont les valeurs de type string, comme nous en avons déjà croisées plus haut. En OCaml, les chaînes sont mutables, à la façon des vecteurs. Cependant, les chaînes ont leur type propre et ne sont pas des vecteurs de caractères (le type string n'est pas unifiable au type char array).

Création de valeurs

On crée une chaîne de caractères en la mettant entre guillemets " :

# "hello world !";;
- : string = "hello world !"

Le caractère \ est utilisé pour indiquer des caractères spéciaux :

# print_endline "\"Entre guillemets\"\nune nouvelle ligne";;
"Entre guillemets"
une nouvelle ligne
- : unit = ()

Il est également possible d'utiliser des fonctions créant des chaînes :

# let chaine = String.make 10 'a';;
val chaine : string = "aaaaaaaaaa"
# string_of_int 42;;
- : string = "42"
# string_of_float 84.;;
- : string = "84."
# Printexc.to_string Not_found (* une chaine à partir d'une exception *);;
- : string = "Not_found"

Accès

Comme pour les valeurs, on peut accéder à l'un des caractères d'une chaîne de deux manières :

Les indices pour une chaîne de longueur n vont de 0 à n−1. En dehors de ces bornes, un accès déclenche là encore une exception Invalid_argument :

# chaine.[10];;
Exception: Invalid_argument "index out of bounds".

Modifications

Il existe deux manières équivalentes de modifier un caractère d'une chaîne :

Autres opérations

D'autres opérations sur les chaînes sont offertes par les fonctions du module String . Par exemple :

# String.length chaine (* longeur d'une chaine *);;
- : int = 10
# String.sub "hello world" 3 4 (* extraction de sous-chaine, comme pour Array.sub *);;
- : string = "lo w"
# let animaux = [ "chat"; "chien"; "perroquet"; "lama"; "renard" ];;
val animaux : string list = ["chat"; "chien"; "perroquet"; "lama"; "renard"]
# String.concat ", " animaux (* concatenation d'une liste de chaines en utilisant un separateur *);;
- : string = "chat, chien, perroquet, lama, renard"

Pour d'autres fonctions, on consultera la documentation du module String .

3.1.3  Champs modifiables des enregistrements

Nous avons vu précédemment (cf. section 2.2.4) comment définir des types enregistrements. Il est possible de spécifier un ou plusieurs champs comme étant modifiables, à l'aide du mot-clé mutable :

# type mon_record = {
champ1 : int ;
mutable champ2 : int ;
};;
type mon_record = { champ1 : int; mutable champ2 : int; }

La création et le filtrage de valeurs de ce type ne changent pas, mais il est maintenant possible de modifier le champ champ2 de ces valeurs :

# let r = { champ1 = 0 ; champ2 = 1 };;
val r : mon_record = {champ1 = 0; champ2 = 1}
# r.champ2 <- 2;;
- : unit = ()
# r;;
- : mon_record = {champ1 = 0; champ2 = 2}

Par contre, tenter de modifier le champ champ1 provoque une erreur :

# r.champ1 <- 3;;
File "", line 1, characters 0-13:
Error: The record field label champ1 is not mutable

3.1.4  Références

«OCaml fournit un type polymorphe 'a ref qui peut être vu comme le type des pointeurs sur une valeur quelconque ; en OCaml, on parlera plutôt de référence sur une valeur. Une valeur référencée peut être modifiée. Le type ref est définit comme un enregistrement à un champ modifiable :»
type 'a ref = {mutable contents:'a}

«Ce type est muni de raccourcis syntaxiques prédéfinis. On construit une référence sur une valeur par la fonction ref. La valeur référencée peut être atteinte par la fonction préfixe (!). La fonction modifiant le contenu d'une référence est la fonction infixe (:=)

# let x = ref 3 (* creation et initialisation *);;
val x : int ref = {contents = 3}
# x ;;
- : int ref = {contents = 3}
# !x (* dereferencement *);;
- : int = 3
# x := 4 ;;
- : unit = ()
# !x ;;
- : int = 4
# x := !x+1 (* affectation *);;
- : unit = ()
# !x ;;
- : int = 5

Remarque : Là encore, pour plus de sûrété, il n'est pas syntaxiquement possible de créer une référence sans l'initialiser à une valeur, contrairement au langage C.

3.2  Entrées-sorties

«Les fonctions d'entrées-sorties calculent une valeur (souvent de type unit), mais durant ce calcul elles effectuent une modification de l'état des périphériques d'entrées-sorties : modification du buffer du clavier, affichage à l'écran, écriture dans un fichier ou modification du pointeur de lecture. Les deux types suivants sont prédéfinis : in_channel et out_channel pour respectivement les canaux d'entrées et de sorties. Quand une fin de fichier est rencontrée l'exception End_of_file est déclenchée. Enfin, les trois constantes suivantes correspondent aux canaux standard d'entrée, de sortie et d'erreur à la manière d'Unix : stdin, stdout et stderr

3.2.1  Canaux

Pour obtenir d'autres canaux que ceux prédéfinis (stdin, stdout et stderr), on utilise :

Les problèmes rencontrés par ces fonctions sont signalés par une levée d'exception Sys_error avec un message explicite, par exemple lorsqu'un fichier à ouvrir en lecture n'existe pas, quand les droits ne sont pas suffisants, etc.

# let ic = open_in "/etc/passwd";;
val ic : in_channel = <abstr>
# let oc = open_out "/tmp/foo";;
val oc : out_channel = <abstr>
# let ic = open_in "/bar";;
Exception: Sys_error "/bar: No such file or directory".

Pour fermer un fichier en lecture ou en écriture, on utilisera respectivement les fonctions close_in et close_out.

On notera que, grâce au typage, on ne pourra utiliser un canal d'entrée (in_channel) pour y écrire des données, ni un canal de sortie (out_channel) pour lire des données.

3.2.2  Lecture

Voici quelques fonctions de lecture sur un canal de type in_channel. D'autres fonctions sont disponibles dans la bibliothèque de base : http://caml.inria.fr/pub/docs/manual-ocaml/libref/Pervasives.html#7_Generalinputfunctions

# let first_line = input_line ic (* lecture d'une ligne *);;
val first_line : string = "root:x:0:0:root:/root:/bin/bash"
# let second_line = input_line ic (* lecture d'une ligne *);;
val second_line : string = "daemon:x:1:1:daemon:/usr/sbin:/bin/sh"
# seek_in ic 0 (* retour au debut du fichier *);;
- : unit = ()
# let line = input_line ic (* re-lecture de la premiere ligne, suite au deplacement *);;
val line : string = "root:x:0:0:root:/root:/bin/bash"
# let buffer_size = 40 ;;
val buffer_size : int = 40
# let buffer = String.make buffer_size 'x';;
val buffer : string = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# let nb_read = input ic buffer 0 buffer_size
(* lecture de maximum buffer_size caracteres dans ic, a stocker dans buffer a partir
de l'indice 0 *);;
val nb_read : int = 40
# String.sub buffer 0 nb_read;;
- : string = "daemon:x:1:1:daemon:/usr/sbin:/bin/sh\nbi"
# close_in ic;;
- : unit = ()

3.2.3  Ecriture

Voici quelques fonctions d'écriture sur un canal de type out_channel. D'autres fonctions sont disponibles dans la bibliothèque de base : http://caml.inria.fr/pub/docs/manual-ocaml/libref/Pervasives.html#7_Generaloutputfunctions

Il est important de savoir que ces canaux sont munis de tampons (buffers). Cela signifie que l'écriture dans un canal correspondant à un fichier n'écrira pas forcément immédiatement dans ce fichier, mais attendra qu'une quantité de données suffisante soit dans le tampon pour procéder à l'écriture réelle, cela afin de conserver des performances suffisantes lors de multiples petites écritures, sans avoir à s'en préoccuper. La fermeture d'un canal provoque la vidange du tampon, de même que la fonction flush.

# output_string oc second_line (* ecriture d'une chaine sur le canal *);;
- : unit = ()
# flush oc (* forcer l'ecriture du contenu du tampon dans le fichier *);;
- : unit = ()
# output oc buffer 0 nb_read (* ecriture d'un certain nombre de caracteres depuis une chaine *);;
- : unit = ()
# close_out oc;;
- : unit = ()

3.3  Structures de contrôle

Nous présentons ci-dessous la séquence d'instructions et les boucles, structures de contrôles de base pour la programmation impérative. La structure conditionnelle a déjà été présentée dans le cadre de la programmation fonctionnelle (cf. section 2.1.3).

3.3.1  Séquence

La séquence d'instructions est représentée par l'utilisation du ';' simple, différent du ';;' de fin de phrase :

expression1 ; expression2

L'expression1 doit être de type unit car sa valeur de retour n'est pas exploitable dans cette construction. Si ce n'est pas le cas, le compilateur émet un avertissement indiquant que la valeur retournée par l'expression n'est pas utilisée. Le type de la séquence ainsi que sa valeur sont ceux de l'expression2. Enfin, l'expression1 est bien sûr évaluée avant l'expression2.

# print_endline chaine;
String.sub buffer 0 nb_read;;
bcaaaaaaaa
- : string = "daemon:x:1:1:daemon:/usr/sbin:/bin/sh\nbi"
# 1 + 1 ; print_endline chaine;;
File "", line 1, characters 0-5:
Warning 10: this expression should have type unit.
bcaaaaaaaa
- : unit = ()

On peut bien sûr composer les séquences :

# print_endline chaine ;
print_endline (String.sub buffer 0 10);
print_endline (String.sub buffer 10 20);
1 + 1;;
bcaaaaaaaa
daemon:x:1
:1:daemon:/usr/sbin:
- : int = 2

La fonction ignore permet d'indiquer explicitement qu'une valeur de retour est volontairement abandonnée :

# ignore(1+1); 42;;
- : int = 42

Si la valeur de la première expression est de type fonctionnel, le compilateur émet un message d'avertissement différent pour indiquer que la fonction n'a peut-être été que partiellement appliquée, par exemple si un paramètre a été oublié :

# let f x y = x + y;;
val f : int -> int -> int = <fun>
# f 1 ; 42;;
File "", line 1, characters 0-3:
Warning 5: this function application is partial,
maybe some arguments are missing.
- : int = 42

Attention aux précédences des constructions !

# if true then
print_endline "c'est vrai";
print_endline "c'est vrai";;
c'est vrai
c'est vrai
- : unit = ()
# if false then
print_endline "c'est vrai";
print_endline "c'est vrai";;
c'est vrai
- : unit = ()

Que se passe-t-il donc ? En fait, la branche then prend fin après le point virgule situé derrière le premier print_endline "c'est vrai". C'est donc équivalent à

# (if false then print_endline "c'est vrai");
print_endline "c'est vrai";;
c'est vrai
- : unit = ()

Il faut donc utiliser les parenthèses ( et ) ou bien les mots-clés begin et end pour forcer la séquence à être dans la branche then :

# if true then
(
print_endline "c'est vrai";
print_endline "c'est vrai"
);;
c'est vrai
c'est vrai
- : unit = ()
# if false then
(
print_endline "c'est vrai";
print_endline "c'est vrai"
);;
- : unit = ()

3.3.2  Boucles

«Les structures de contrôle itératives sont elles aussi en dehors du monde fonctionnel. La condition de répétition, ou de sortie, d'une boucle n'a de sens que si une modification physique de la mémoire permet d'en changer la valeur. Il existe deux structures de contrôle itératives : la boucle for pour une itération bornée et la boucle while pour une itération non bornée. Les structures de boucle elles-mêmes sont des expressions du langage. Elles retournent donc une valeur : la constante () du type unit

Boucle for

La boucle for peut être croissante :

for <identifiant> = <expression1> to <expression2> do <expression3> done

ou décroissante :

for <identifiant> = <expression1> downto <expression2> do <expression3> done

La valeur de l'identifiant croît ou décroît toujours d'un pas de un. L'identifiant n'est visible que dans l'expression3 (et masque éventuellement une variable de même nom présente dans l'environnement). expression1 et expression2 doivent être de type int. L'expression3 doit être de type unit car sa valeur est ignorée et l'expression dans la boucle n'a de sens que si elle a un effet de bord. Si l'expression n'est pas de type unit, le compilateur émet un avertissement.

# for i = 10 downto 1 do print_int i; print_string " " done; flush stdout;;
10 9 8 7 6 5 4 3 2 1 - : unit = ()

Boucle while

La syntaxe de cette boucle est la suivante :

while <expression1> do <expression2> done

L'expression1 doit être de type bool. Elle est évaluée au début de chaque tour de boucle. Si elle est vraie, l'expression2 est évaluée, sinon la boucle se termine en une valeur de type unit. Comme pour le corps de la boucle for, l'expression2 doit être de type unit, sinon le compilateur émet un avertissement.

# let compteur = ref 1;;
val compteur : int ref = {contents = 1}
# while !compteur <= 10 do
print_int !compteur; print_string " ";
incr compteur (* equivalent à compteur := !compteur + 1 *)
done;;
- : unit = ()
# flush stdout;;
1 2 3 4 5 6 7 8 9 10 - : unit = ()

3.4  Exercices

Exercice 8. Ecrire un programme qui affiche ses arguments, à la manière de la commande UNIX echo. Pour cela, on utilisera Sys.argv qui est le tableau des arguments du programme, ainsi que le module Array pour les accès aux tableaux. Le parcours du tableau sera réalisé à l'aide d'une boucle. On compilera le programme grâce à la commande
# ocamlc -o myecho myecho.ml
for i = 1 to Array.length Sys.argv - 1 do
print_string Sys.argv.(i); print_string " "
done;;
print_newline();;

Exercice 9. En utilisant une boucle, écrire une fonction blank qui prend une chaîne de caractères, la copie et remplace chaque caractère par un blanc, tant qu'elle n'a pas rencontré la lettre 's'.
# let blank str =
let str = String.copy str in
let i = ref 0 in
let len = String.length str in
while !i < len do
if str.[!i] = 's' then
i := len
else
( str.[!i] <- ' ' ; incr i )
done;
str
;;
val blank : string -> string = <fun>

Tester ensuite la fonction, par exemple :

# let str = "coucous_royal";;
val str : string = "coucous_royal"
# blank str;;
- : string = " s_royal"

Exercice 10. On souhaite écrire une fonction prenant en paramètre une matrice de type 'a array array et retournant true ou false selon que la matrice est symétrique ou non. On suppose que la matrice est carrée. On utilisera les matrices mat1 et mat2 définies ci-dessous pour tester :

(* matrice symétrique: *)
let mat1 = [| [| 0 ; 1 ; 2 |] ; [| 1 ; 5 ; 10 |] ; [| 2 ; 10 ; 8 |] |];;

let init _ = Random.int 10 ;;

(* matrice non symétrique *)
let mat2 = [| Array.init 3 init ; Array.init 3 init ; Array.init 3 init |];;

mat2.(0).(1) <- - mat2.(1).(0);;

On commence par écrire une fonction is_sym_imper impérative.

# let is_sym_imper mat =
let len = Array.length mat in
let res = ref true in
for i = 0 to len - 1 do
for j = i + 1 to len - 1 do
if mat.(i).(j) <> mat.(j).(i) then res := false
done
done;
!res
;;
val is_sym_imper : 'a array array -> bool = <fun>
# is_sym_imper mat1;;
- : bool = true
# is_sym_imper mat2;;
- : bool = false

On donnera ensuite une version fonctionnelle is_sym_fun.

# let is_sym_fun mat =
let len = Array.length mat in
let rec iterj i j =
(j > len - 1) ||
(mat.(i).(j) = mat.(j).(i) && iterj i (j+1))
in
let rec iteri i =
(i > len - 1) ||
(iterj i (i+1) && iteri (i+1))
in
iteri 0
;;
val is_sym_fun : 'a array array -> bool = <fun>
# is_sym_fun mat1;;
- : bool = true
# is_sym_fun mat2;;
- : bool = false

Chapitre 4  Bibliothèques

Comme beaucoup d'autres distributions de compilateurs/langages, OCaml est accompagné de bibliothèques. Chaque bibliothèque peut définir de nouveaux types, nouvelles exceptions, nouvelles fonctions, nouvelles classes et nouveaux modules. Ces bibliothèques soit évitent au développeur d'avoir à réimplementer des fonctions de base, soit fournissent des opérations non définissables dans le langage comme les fonctions d'entrées/sorties à un nombre variable d'arguments (e.g. printf).

Elles se présentent sous forme de modules incluant éventuellement d'autres modules. Nous allons voir ici comment utiliser ces modules, tandis que la définition de nouveaux modules est détaillée dans le chapitre 5.

Les bibliothèques de la distribution OCaml sont séparées en trois ensembles :

4.1  Bibliothèque préchargée

Le module Pervasives est la bibliothèque préchargée par OCaml lors de la compilation ou l'évaluation de programmes, c'est-à-dire que tout se passe comme si en tête de chaque fichier de code figurait une instruction

open Pervasives;;

Les éléments de Pervasives sont donc visibles et accessibles sans nécessité de les préfixer (on peut faire appel à compare au lieu de Pervasives.compare). La construction open ... est présentée en 4.2.

Dans la documentation anglaise, cette bibliothèque est appelée "Core library".

Elle fournit des éléments de base dans des domaines variés. Nous en donnons quelques-uns ci-dessous mais il sera utile de parcourir la documentation du module pour avoir un bon aperçu de ce qu'il offre.

4.1.1  Types et exceptions

C'est dans ce module que sont définis les types de base ainsi que les exceptions d'usage général.

4.1.2  Comparaisons

Plusieurs fonctions de comparaisons sont fournies et sont polymorphes, permettant ainsi de comparer n'importe quel couple de valeur d'un même type :

(=) : 'a -> 'a -> boolEgalité structurelle
(<>)Négation de (=)
(<) et (<=)relation d'infériorité structurelle stricte ou non
(>) et (>=)relation de supériorité structurelle stricte ou non
commpare : 'a -> 'a -> intComparaison générale :
compare a b = 0 si a = b
compare a b < 0 si a < b
compare a b > 0 si a > b
min : 'a -> 'a -> 'aMinimum structurel de deux valeurs
maxMaximum structurel de deux valeurs
(==) : 'a -> 'a -> boolEgalité physique
(!=)Négation de (==)

Les fonctions entre parenthèses sont utilisables en position infixe.

Quelques exemples :

# "toto" = "tutu";;
- : bool = false
# "abc" < "def";;
- : bool = true
# 10 >= 13 ;;
- : bool = false
# compare [1;2] [1;2;3];;
- : int = -1
# "toto" = "toto";;
- : bool = true
# "toto" == "toto";;
- : bool = false

4.1.3  Fonctions sur les booléens

Les fonctions classiques sur les booléens sont disponibles :

not : bool -> boolnon logique
(&&) : bool -> bool -> boolet logique
(||)ou logique

4.1.4  Fonctions sur les entiers

On trouve les opérations arithmétiques habituelles sur les entiers :

(+), (-) : int -> int -> intaddition, soustraction
(*), (/), (mod)multiplication, division entière, modulo
~- : int -> intnégation
succ, pred : int -> intsuccesseur, prédécesseur
abs : int -> intvaleur absolue

Les débordements (overflow) sont ignorés. Dans les cas où ils doivent être gérés, on pourra utiliser les constantes min_int et max_int qui sont respectivement les plus petit et plus grand entiers représentables sur la plateforme d'exécution.

4.1.5  Opérations bit à bit

Des fonctions sont disponibles pour manipuler les entiers au niveau des bits, pour l'utilisation de masques, gestion de protocoles ou données binaires, …

lnot : int -> intnégation bit à bit logique
(lor), (land) : int -> int -> intou et et bit à bit logique
(lxor)ou exclusif bit à bit logique
(lsl), (lsr), (asr)décalages à gauche et à droite

4.1.6  Fonctions sur les flottants

Il n'y a pas de surchage en OCaml, c'est-à-dire que le même identifiant ne peut pas correspondre à deux fonctions différentes selon le type de ses arguments. Les opérations communes aux entiers et aux flottants (addition, …) se distinguent donc par un '.' ajouté aux opérateurs flottants :

(+.), (-.) : float -> float -> floataddition, soustraction
(*.), (/.), mod_floatmultiplication, division, modulo
 -. : float -> floatnégation
abs_float : float -> floatvaleur absolue
sqrt : float -> floatracine carrée
(**) : float -> float -> floatpuissance
float_of_int : int -> floatcréation d'un flottant depuis un entier
truncate : float -> intcréation d'un entier à partir de la partie entière d'un flottant

Il existe des valeurs spéciales pour indiquer des valeurs qui ne sont pas des nombres ("not a number") : infinity, neg_infinity, …Ces valeurs sont obtenues en effectuant certaines opérations comme une division par 0.0.

Beaucoup d'autres fonctions sont disponibles sur les flottants (trigonométrie, …).

4.1.7  Entrées/Sorties

Un certain nombre de fonctions d'entrées/sorties sont disponibles dans la bibliothèque préchargée. Certaines fonctions interagissent avec l'entrée standard, la sortie standard ou la sortie d'erreur, tandis que d'autres sont plus générales et agissent sur un canal (channel) passé en paramètre.

A l'aide de la documentation de Pervasives , faire les exercices suivants.

Exercice 11. Ecrire un programme qui affiche la somme de deux entiers saisis par l'utilisateur sur l'entrée standard, sous la forme "La somme de A et B est C." avec un retour chariot. On pourra lancer son programme par la commande
# ocaml fichier.ml
let a = read_int ();;
let b = read_int ();;
print_string "La somme de ";;
print_int a;;
print_string " et ";;
print_int b;;
print_string " est ";;
print_int (a + b);;
print_string ".\n";;

Exercice 12. Ecrire un programme se comportant comme une simplification de la commande UNIX cat : il affichera sur sa sortie standard ce qu'il lit sur son entrée standard. On traitera proprement la détection de la fin de lecture.
try
while true do print_endline (read_line ()) done
with
End_of_file -> exit 0;;

4.2  Bibliothèque standard

La bibliothèque dite "standard" contient des modules disponibles sur toutes les plateformes sur lesquelles OCaml fonctionne. Elle est donc un socle stable pour développer des applications portables.

L'utilisation des éléments d'un module se fait en préfixant l'élément (fonction, type, …) par le nom du module suivi d'un point, comme dans List.split ou String.lowercase. Il est également possible d'ouvrir le module, par l'instruction

open Nom_du_module;;

pour rendre tous ses éléments visibles après le open.

Attention, dans ce cas, les éléments du module ouvert masquent les éléments de même nom visibles auparavant :

# let length x = x + 1;;
val length : int -> int = <fun>
# length 10;;
- : int = 11
# open List;;
# length 10;;
File "", line 1, characters 7-9:
Error: This expression has type int but an expression was expected of type
'a list

En général, une bonne pratique est de ne pas ouvrir de module, mais de préfixer les éléments par leur nom complet afin de faciliter la lecture du programme. En effet, la présence du nom du module permet de plus facilement déduire quelle structure de données est manipulée, puisqu'il n'est pas nécessaire en OCaml d'indiquer explicitement les types. On peut faire une exception pour les constructeurs de type et les champs d'enregistrements, c'est-à-dire ouvrir un module pour ne pas avoir à préfixer les constructeurs et champs mais préfixer tout de même les autres éléments lors de leur utilisation.

Enfin, il n'y a pas de hiérarchie dans cette bibliothèque, les modules disponibles sont tous au même niveau (même si certains modules en contiennent d'autres). Nous allons cepandant les introduire par grandes catégories et détailler les possibilités de quelques uns d'entre eux.

4.2.1  Structures de données

La bibliothèque standard offre plusieurs modules permettant de construire et manipuler des structures de données courantes. Nous avons déjà vu précédemment les modules suivants :

Le module Buffer permet de manipuler des tampons extensibles de caractères, dans un style impératif, plus efficaces dans certains algorithmes que des concaténations à répétition de chaînes de caractères.

On remarquera que les vecteurs et les listes sont génériques puisqu'ils permettent de manipuler des vecteurs et des listes d'éléments arbitraires (listes d'entiers, listes de chaînes, …).

D'autres structures de données sont disponibles et offrent aussi cette généricité.

Exercice 13. Ecrire une fonction is_palindrome prenant en paramètre une chaîne de caractères et retournant true si c'est un palindrome, false sinon. Une chaîne est un palindrome si elle est symétrique (e.g. "radar", "laval").
# let is_palindrome s =
let len = String.length s in
let rec test i =
(i > (len + 1) / 2) ||
(s.[i] = s.[len - i - 1] && test (i+1))
in
test 0;;
val is_palindrome : string -> bool = <fun>
# is_palindrome "nonacecanon";;
- : bool = true
# is_palindrome "anna";;
- : bool = true
# is_palindrome "baobab";;
- : bool = false

Exercice 14. On peut spécifier des paramètres optionnels pour les fonctions, avec ou non une valeur par défaut.

Dans let f ?x y = ..., x sera une valeur du type option. Le type option est prédéfini ainsi:
type 'a option = None | Some of 'a
et permet de manipuler des valeurs optionnelles. On pourra donc utiliser le filtrage pour procéder par cas sur la structure de x :

# let f ?x y = match x with None -> y | Some x -> x + y;;
val f : ?x:int -> int -> int = <fun>

Lors de l'appel d'une telle fonction, on pourra ou non préciser la valeur du paramètre optionnel :

# f 4;;
- : int = 4
# f x: 1 4;;
- : int = 5

Pour donner une valeur par défaut à un paramètre, on pourrait utiliser la forme suivante :

# let f ?x =
let x = match x with None -> 1 | Some x -> x in
fun y -> x + y;;
val f : ?x:int -> int -> int = <fun>

On préférera la syntaxe suivante, plus simple et lisible :

# let f ?(x=1) y = x + y;;
val f : ?x:int -> int -> int = <fun>

La façon de préciser la valeur de l'argument lors de l'application de f ne change pas :

# f 4;;
- : int = 5
# f x: 2 4;;
- : int = 6

Ecrire une fonction list_remove_doubles prenant en paramètre une liste et retournant la même liste mais dans laquelle chaque élément apparaît au plus une fois. La fonction acceptera un paramètre optionnel comme fonction de comparaison pour savoir si deux éléments sont identiques. Par défaut ce paramètre sera la fonction d'égalité polymorphe (=).

# let list_remove_doubles ?(pred=(=)) l =
List.rev
(List.fold_left
(fun acc e -> if List.exists (pred e) acc then acc else e :: acc)
[]
l
)
;;
val list_remove_doubles : ?pred:('a -> 'a -> bool) -> 'a list -> 'a list =
<fun>

# list_remove_doubles [ 1 ; 1 ; 3; 2 ; 2; 2 ; 1; 2; 3; 4];;
- : int list = [1; 3; 2; 4]
# let comp_string s1 s2 = String.lowercase s1 = String.lowercase s2;;
val comp_string : string -> string -> bool = <fun>
# list_remove_doubles pred: comp_string
[ "hello"; "HELlo";"world";"heLLO"; "WorLD"; "!"; "!"; "hello"];;
- : string list = ["hello"; "world"; "!"]

Exercice 15. Ecrire une fonction prenant une chaîne de caractères et retournant la liste des mots qu'elle contient. Un mot sera composé uniquement des caractères 'a' à 'z', 'A' à 'Z', 'é', 'ê', 'è', 'à'.
# let words s =
let len = String.length s in
let rec iter acc_words acc_current i =
if i >= len then
match acc_current with
"" -> List.rev acc_words
| w -> List.rev (w :: acc_words)
else
match s.[i] with
| 'a'..'z' | 'A'..'Z' | 'é' | 'è' | 'ê' | 'à' ->
iter acc_words (Printf.sprintf "%s%c" acc_current s.[i]) (i+1)
| _ ->
match acc_current with
"" -> iter acc_words acc_current (i+1)
| w -> iter (w::acc_words) "" (i+1)
in
iter [] "" 0
;;
val words : string -> string list = <fun>

# words "maitre corbeau sur un arbre perché ... par l'odeur alléché...";;
- : string list =
["maitre"; "corbeau"; "sur"; "un"; "arbre"; "perch\233"; "par"; "l"; "odeur";
"all
\233ch\233"]

Tables de hachage

Le module Hashtbl permet d'utiliser des tables de hachage. Les tables de hachage sont des structures de données permettant de stocker des associations clé-valeur, avec modification en place. L'ajout d'une valeur pour une clé existante masque la valeur précédente associée à cette clé. Les clés et les valeurs peuvent être de n'importe quel type, tant que l'on sait comment comparer les clés (égalité) et que l'on a une fonction pour calculer un indice à partir d'une clé (fonction de hachage). En effet, les valeurs ne sont pas stockées dans un tableau de taille infinie, mais au contraire dans un tableau de taille finie, et la fonction de hachage doit permettre une répartition la plus homogène possible dans le tableau, afin d'avoir des temps d'accès les plus courts. Si deux clés ont la même valeur de hachage, les deux associations clé-valeur sont stockées dans une liste à l'indice commun du tableau. La taille de la table doit donc être adaptée au nombre d'associations à stocker. Si elle est trop petite, les conflits dans les hash de clés seront nombreux et au final on parcourra souvent les listes d'associations ayant la même valeur de hachage. Si elle est trop grande, elle prend inutilement de la place. Enfin, on conseille de prendre un nombre premier comme taille de la table.

Un type de table de hachage générique est disponible, utilisant des fonctions d'égalité et de hachage prédéfinies. Il est également possible de définir son propre type de table de hachage spécialisé pour un type de données. Pour cela, on définit un module contenant les fonctions d'égalité et de hachage et on passe ce module à un autre module ( Hashtbl.Make ), afin de construire un nouveau module permettant la manipulation de tables de hachage spécialisées par les fonctions du module en paramètre. Les modules prenant d'autres modules en paramètres sont appelés foncteurs. Ils sont exposés dans la section 5.2.

Voici un exemple d'utilisation de table de hachage :

# type person = { name : string ; firstname : string };;
type person = { name : string; firstname : string; }
# let users = Hashtbl.create 101;;
val users : ('_a, '_b) Hashtbl.t = <abstr>
# let robert = { name = "Bidochon" ; firstname = "Robert"};;
val robert : person = {name = "Bidochon"; firstname = "Robert"}
# Hashtbl.add users "bidochon" robert;;
- : unit = ()
# Hashtbl.find users "bidochon";;
- : person = {name = "Bidochon"; firstname = "Robert"}
# let raymonde = { name = "Bidochon" ; firstname = "Raymonde"};;
val raymonde : person = {name = "Bidochon"; firstname = "Raymonde"}
# Hashtbl.add users "bidochon" raymonde (* robert est masque *);;
- : unit = ()
# Hashtbl.find users "bidochon";;
- : person = {name = "Bidochon"; firstname = "Raymonde"}
# Hashtbl.remove users "bidochon" (* robert est demasque *);;
- : unit = ()
# Hashtbl.find users "bidochon";;
- : person = {name = "Bidochon"; firstname = "Robert"}
# Hashtbl.remove users "bidochon" (* robert est supprime *);;
- : unit = ()
# Hashtbl.find users "bidochon";;
Exception: Not_found.

Le type de la table users à sa création a le type ('_a, '_b) Hashtbl.t : users est du type Hashtbl.t mais les deux paramètres de type ne sont pas encore fixés (on a ‘_a au lieu d'un ‘a qui indiquerait le polymorphisme), ils le seront dès que la suite du programme mettra des contraintes sur ces types. On peut d'ailleurs le vérifier maintenant que la table a été utilisée :

# users;;
- : (string, person) Hashtbl.t = <abstr>

L'indication = <abstr> signifie que le type est abstrait. C'est le cas de beaucoup de structures de données prédéfinies : leur représentation interne est masquée et il faut donc obligatoirement passer par les fonctions fournies pour manipuler ces structures. Cet aspect sera exposé dans la partie sur les modules dans le chapitre 5.

Exercice 16. Ecrire un programme prenant en paramètre un nom de fichier et comptant le nombre d'occurrences de chaque mot apparaissant dans le fichier. A la fin, le programme affiche chaque mot avec son nombre d'apparitions. On pourra utiliser la fonction words définie dans l'exercice 15.
# (** la fonction suivante incremente le compteur du mot en parametre.
si le mot n'est pas dans la table, il est ajoute avec un compteur a 1. *)
let add_word table w =
try
let n = Hashtbl.find table w in
Hashtbl.replace table w (n+1)
with Not_found ->
Hashtbl.add table w 1
;;
val add_word : ('a, int) Hashtbl.t -> 'a -> unit = <fun>
# (** la fonction de comptage des mots d'un fichier en parametre.
Pour chaque ligne, on recupere la liste des mots avec la fonction 'words',
puis on incremente le compteur de chaque mot avec la fonction 'add_word'
definie ci-dessus. On lit jusqu'a la fin du fichier (exception End_of_file)
et on retourne la table de comptage. *)
let count_in_file file =
let table = Hashtbl.create 101 in
let ic = open_in file in
try
while true do
let line = input_line ic in
List.iter (add_word table) (words line)
done;
assert false
with
End_of_file -> close_in ic; table
;;
val count_in_file : string -> (string, int) Hashtbl.t = <fun>
# let print_words =
Hashtbl.iter
(fun wrd n -> Printf.printf "%s: %d\n" wrd n)
;;
val print_words : (string, int) Hashtbl.t -> unit = <fun>
# let table = count_in_file "texte.txt"(* remplacer par Sys.argv.(1) pour le fichier en parametre *);;
val table : (string, int) Hashtbl.t = <abstr>
# print_words table;;
- : unit = ()
# flush stdout;;
son: 1
alléché: 1
lui: 1
l: 1
en: 1
un: 2
maitre: 2
renard: 1
sur: 1
bec: 1
près: 1
langage: 1
tenait: 1
odeur: 1
arbre: 1
fromage: 1
peu: 1
ce: 1
par: 1
corbeau: 1
perché: 1
tint: 1
a: 1
- : unit = ()

Dictionnaires

Ces structures de données permettent d'associer des valeurs à des clés, comme pour les tables de hachage mais dans un style applicatif, sans effet de bord. Elles sont basées sur des arbres binaires et nécessitent pour cela de savoir comment ordonner les clés des paires (clé, valeur) à ranger.

On crée un module pour gérer ce genre de structure en utilisant le foncteur Map.Make , du module Map . On peut utiliser la fonction générique Pervasives.compare comme fonction d'ordre, notamment quand on utilise comme clé un type de données prédéfini pour lequel cette fonction a le comportement souhaité :

# module MyKey = struct
type t = int
let compare = Pervasives.compare
end;;
module MyKey : sig type t = int val compare : 'a -> 'a -> int end
# module MyMap = Map.Make(MyKey);;
module MyMap :
sig
type key = MyKey.t
type 'a t = 'a Map.Make(MyKey).t
val empty : 'a t
val is_empty : 'a t -> bool
val mem : key -> 'a t -> bool
val add : key -> 'a -> 'a t -> 'a t
val singleton : key -> 'a -> 'a t
val remove : key -> 'a t -> 'a t
val merge :
(key -> 'a option -> 'b option -> 'c option) -> 'a t -> 'b t -> 'c t
val compare : ('a -> 'a -> int) -> 'a t -> 'a t -> int
val equal : ('a -> 'a -> bool) -> 'a t -> 'a t -> bool
val iter : (key -> 'a -> unit) -> 'a t -> unit
val fold : (key -> 'a -> 'b -> 'b) -> 'a t -> 'b -> 'b
val for_all : (key -> 'a -> bool) -> 'a t -> bool
val exists : (key -> 'a -> bool) -> 'a t -> bool
val filter : (key -> 'a -> bool) -> 'a t -> 'a t
val partition : (key -> 'a -> bool) -> 'a t -> 'a t * 'a t
val cardinal : 'a t -> int
val bindings : 'a t -> (key * 'a) list
val min_binding : 'a t -> key * 'a
val max_binding : 'a t -> key * 'a
val choose : 'a t -> key * 'a
val split : key -> 'a t -> 'a t * 'a option * 'a t
val find : key -> 'a t -> 'a
val map : ('a -> 'b) -> 'a t -> 'b t
val mapi : (key -> 'a -> 'b) -> 'a t -> 'b t
end
# let map = MyMap.empty;;
val map : 'a MyMap.t = <abstr>
# let map = MyMap.add 1 "coucou" map;;
val map : string MyMap.t = <abstr>
# let map = MyMap.add 2 "bonjour" map;;
val map : string MyMap.t = <abstr>
# let map = MyMap.add 3 42 map;;
File "", line 1, characters 25-28:
Error: This expression has type string MyMap.t = string Map.Make(MyKey).t
but an expression was expected of type
int MyMap.t = int Map.Make(MyKey).t
# MyMap.iter
(fun key value -> Printf.printf "cle=%d, valeur=%s\n" key value)
map;;
- : unit = ()
# flush stdout;;
cle=1, valeur=coucou
cle=2, valeur=bonjour
- : unit = ()

Exercice 17. Reprendre l'exercice 16 en utilisant un dictionnaire.
(** nous utiliserons un dictionnaire avec des string comme cles *)
module Dict = Map.Make
(struct type t = string let compare = Pervasives.compare end);;

(** la fonction suivante incremente le compteur du mot en parametre.
si le mot n'est pas dans le dictionnaire, il est ajoute avec un compteur a 1. *)
let add_word dict wrd =
try
let n = Dict.find wrd dict in
Dict.add wrd (n+1) dict
with Not_found ->
Dict.add wrd 1 dict
;;

(** la fonction de comptage des mots d'un fichier en parametre.
Pour chaque ligne, on recupere la liste des mots avec la fonction 'words',
puis on incremente le compteur de chaque mot avec la fonction 'add_word'
definie ci-dessus. On lit jusqu'a la fin du fichier (exception End_of_file)
et on retourne le dictionnaire. *)
let count_in_file file =
let ic = open_in file in
let rec read_lines dict =
let line_opt =
try Some (input_line ic)
with End_of_file -> None
in
(* le rattrapage de l'exception de fin de fichier est confine
pour permettre la recursivite terminale *)
match line_opt with
None -> dict
| Some line ->
let dict = List.fold_left add_word dict (words line) in
read_lines dict
in
let dict = read_lines Dict.empty in
close_in ic;
dict
;;

let print_words =
Dict.iter
(fun wrd n -> Printf.printf "%s: %d\n" wrd n)
;;

let dict = count_in_file "texte.txt"(* remplacer par Sys.argv.(1) pour le fichier en parametre *);;

print_words dict;;

flush stdout;;

Ensembles

Le module Set permet de définir des structures de données représentant des ensembles de valeurs d'un même type. Il est alors possible d'obtenir le plus grand élément, le plus petit, de faire des unions, intersections, ….

De même que pour les dictionnaires, on crée un module de gestion d'ensembles à l'aide d'un foncteur, Set.Make . Le module passé en paramètre doit définir le type des éléments et la fonction d'ordre sur ces éléments. On utilise dans l'exemple ci-dessous le même module MyKey que dans l'exemple des dictionnaires, pour faire un module de manipulation d'ensembles d'entiers :

# Random.self_init();;
- : unit = ()
# module IntSet = Set.Make(MyKey);;
module IntSet :
sig
type elt = MyKey.t
type t = Set.Make(MyKey).t
val empty : t
val is_empty : t -> bool
val mem : elt -> t -> bool
val add : elt -> t -> t
val singleton : elt -> t
val remove : elt -> t -> t
val union : t -> t -> t
val inter : t -> t -> t
val diff : t -> t -> t
val compare : t -> t -> int
val equal : t -> t -> bool
val subset : t -> t -> bool
val iter : (elt -> unit) -> t -> unit
val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a
val for_all : (elt -> bool) -> t -> bool
val exists : (elt -> bool) -> t -> bool
val filter : (elt -> bool) -> t -> t
val partition : (elt -> bool) -> t -> t * t
val cardinal : t -> int
val elements : t -> elt list
val min_elt : t -> elt
val max_elt : t -> elt
val choose : t -> elt
val split : elt -> t -> t * bool * t
end
# let rec f set i =
if i <= 100 then
f (IntSet.add (Random.int 4000) set) (i+1)
else
set;;
val f : IntSet.t -> int -> IntSet.t = <fun>
# let set = f IntSet.empty 1;;
val set : IntSet.t = <abstr>
# Printf.printf "Plus petit element tiré: %d, plus grand: %d\n"
(IntSet.min_elt set) (IntSet.max_elt set);;
- : unit = ()
# flush stdout;;
Plus petit element tiré: 36, plus grand: 3951
- : unit = ()
# let sum = IntSet.fold (+) set 0;;
val sum : int = 178828

Exercice 18. Ecrire un programme prenant deux fichiers et affichant la liste des mots en commun sur la sortie standard. On utilisera encore la fonction words de l'exercice 15.
(** nous creons un module pour representer les ensemble de mots *)
module Words = Set.Make
(struct type t = string let compare = Pervasives.compare end);;

(** la fonction suivante cree l'ensemble des mots du fichier en parametre.
Pour chaque ligne, on recupere la liste des mots avec la fonction 'words'
et on ajoute chacun de ces mots a l'ensemble en cours de construction.
A la fin du fichier (exception End_of_file), on retourne l'ensemble
construit. *)
let word_set_of_file file =
let ic = open_in file in
let rec read_lines set =
let line_opt =
try Some (input_line ic)
with End_of_file -> None
in
(* le rattrapage de l'exception de fin de fichier est confine
pour permettre la recursivite terminale *)
match line_opt with
None -> set
| Some line ->
let set = List.fold_left
(fun set wrd -> Words.add wrd set)
set (words line)
in
read_lines set
in
let set = read_lines Words.empty in
close_in ic;
set
;;

let print_words = Words.iter print_endline ;;

let set1 = word_set_of_file "texte.txt" (* remplacer par Sys.argv.(1) *);;

let set2 = word_set_of_file "texte2.txt" (* et Sys.argv.(2) *) ;;

print_words (Words.inter set1 set2);;

Files d'attente

Le module Queue implémente les files d'attente (ou FIFO, pour First In First Out). Le type Queue.t des files est donc paramétré par le type des éléments dans la file, de la même façon que le type list. Les modifications des files sont faites en place (par effet de bord).

Piles

Le module Stack implémente les piles, là encore avec un type Stack.t paramétré par le type des éléments stockés dans la pile. Les modifications sont faites en place.

Remarques

On peut remarquer que certaines conventions de nommage sont respectées dans la bibliothèque standard. Ainsi, les modules définissant un type de données le nomment t, et les fonctions similaires partagent le même nom :

L'ordre des arguments est aussi cohérent. Ces conventions facilitent l'utilisation d'un module à la place d'un autre pour changer de structure de données. Nous verrons un exemple dans la section sur les foncteurs (section 5.2).

4.2.2  Calcul

Le module Complex définit un type de données pour les nombres complexes et des fonctions pour les manipuler.

Différents modules permettent d'effectuer des calculs sur des entiers avec des représentations différentes du type int :

4.2.3  Entrées-sorties

Le module Printf donne accès aux fonctions équivalentes aux fonctions printf, sprintf, fprintf, etc. du langage C. Par exemple :

# let str = Printf.sprintf "La somme de %d et %d est %d" 1 2 (1+2);;
val str : string = "La somme de 1 et 2 est 3"
# let buffer = Buffer.create 256;;
val buffer : Buffer.t = <abstr>
# Printf.bprintf buffer "Le produit de %d et %d donne %d" 4 5 (4*5);;
- : unit = ()
# Buffer.contents buffer;;
- : string = "Le produit de 4 et 5 donne 20"

Le type des paramètres des fonctions du module Printf est spécial, pour permettre le passage d'un nombre variable de paramètres en fonctions de la chaîne de format donnée :

# Printf.printf;;
- : ('a, out_channel, unit) format -> 'a = <fun>

Comme en C, des fonctions de lecture de valeurs de différents types depuis des chaînes sont disponibles, dans le module Scanf . Dans l'exemple suivant, la fonction en paramètre de sscanf prend deux arguments entiers car la chaîne de format contient deux %d. La cohérence de type entre la chaîne de format et la fonction données est réalisée par le compilateur :

# let str = "1 + 2";;
val str : string = "1 + 2"
# let sum = Scanf.sscanf str "%d + %d" (fun a b -> a + b) ;;
val sum : int = 3

Des fonctionnalités de formatage de sortie texte plus élaborées sont disponibles dans le module Format  : définition de marges, retour à la ligne automatique en cas de dépassement de la largeur maximum imposée, manipulation de "boîtes" de pretty-print, ….

Enfin, l'échange, le stockage et la lecture de données peuvent se faire en utilisant les fonctions génériques de sérialisation du module Marshal . Les données ne doivent cependant pas contenir de fonctions.

4.2.4  Interface avec le système

Analyse de la ligne de commande

Le module Arg offre des facilités pour analyser la ligne de commande de lancement du programme.

Exercice 19. Ecrire un programme qui accepte deux entiers sur la ligne de commande ainsi qu'une option -prod. Le programme affiche la somme des deux entiers, ou bien le produit si l'option -prod est passée. On écrira le code dans un fichier fichier.ml et on l'exécutera par exemple par la commande
# ocaml fichier.ml -prod 1 2

let prod = ref false;;
let options =
[ "-prod", Arg.Set prod, " calcul du produit au lieu de la somme" ];; let read_options () =
let args = ref [] in
Arg.parse options
(fun s -> args := s :: !args)
(Printf.sprintf "Utilisation: %s [-prod] a b" Sys.argv.(0));
match !args with
[a;b] ->
begin
try (int_of_string a, int_of_string b)
with _ -> failwith "Un argument n'est pas un entier"
end
| _ -> failwith "Il faut deux et seulement deux entiers en arguments"
;; let (a, b) =
try read_options ()
with Failure msg -> prerr_endline msg; exit 1;; let result = (if !prod then ( * ) else ( + )) a b;;
print_int result;;

Noms de fichiers

Le module Filename contient des fonctions de manipulation des noms de fichiers, utiles notamment pour produire du code portable sur UNIX/Linux/Mac OS X et Windows en fournissant les fonctions current_dir_name, parent_dir_name, concat, dirname, …. Par exemple :

# let file = "/home/foo/file.txt";;
val file : string = "/home/foo/file.txt"
# let dir = Filename.dirname file;;
val dir : string = "/home/foo"
# let bname = Filename.basename file;;
val bname : string = "file.txt"
# Filename.concat dir bname;;
- : string = "/home/foo/file.txt"
# Filename.check_suffix file "txt";;
- : bool = true

Gestion des exceptions

Le module Printexc offre des fonctions de convenance pour traiter et afficher les exceptions, comme par exemple Printexc.to_string qui retourne une chaîne pour représenter l'exception ou Printexc.print qui affiche une exception non rattrapée lors de l'application d'une fonction et relève l'exception :

# int_of_string "bouh!";;
Exception: Failure "int_of_string".
# Printexc.print int_of_string "bouh!";;
Uncaught exception: Failure("int_of_string")
Exception: Failure "int_of_string".

Contrôle du ramasse-miettes

Le module Gc permet de modifier le comportement du ramasse-miettes ou glaneur de cellules (garbage collector en anglais) et d'obtenir diverses statistiques sur la gestion de la mémoire. Il est également possible d'indiquer un traitement à effectuer lorsque la mémoire occupée par une valeur est libérée par le GC (lorsque cette valeur n'est plus accessible).

Interface avec le système d'exploitation

Le module Sys offre des fonctions portables de communication avec le système d'exploitation concernant le système de fichiers, le répertoire courant, les signaux, certaines constantes comme la taille des mots, la longueur maximale d'une chaîne de caractères, etc. On trouve également dans ce module Sys.argv qui est le tableau des arguments de la ligne de commande. Comme en C, Sys.argv.(0) est le nom de la commande lancée.

# print_endline (Sys.getcwd());;
/home/guesdon/devel/form-ocaml
- : unit = ()
# Sys.chdir "/tmp";;
- : unit = ()
# print_endline (Sys.getcwd());;
/tmp
- : unit = ()
# Sys.command "ls | head -n 3";;
claws-mail-1000
fixpoint0.txt
fixpoint1.txt
- : int = 0

4.2.5  Utilitaires

Quelques autres modules sont indiqués ici à titre informatif :

4.3  Autres bibliothèques de la distribution

La distribution contient encore d'autres bibliothèques, dont certaines disponibles selon le système d'exploitation et les bibliothèques installées :

Lorsqu'on utilise ces bibliothèques dans un programme OCaml, il faut ajouter ces bibliothèques sur la ligne de commande de création de l'exécutable, à la façon des bibliothèques libXXX.a en C. Une bibliothèque B dépendant d'une bibliothèque A doit être placée après la bibliothèque dont elle dépend :

# ocamlc -o mon_exe a.cma b.cma module1.cmo ...
pour la compilation code-octet, ou
# ocamlopt -o mon_exe a.cmxa b.cmxa module1.cmx ...
pour la compilation en code natif.

Exercice 20. Ecrire un programme qui affiche la liste des fichiers du répertoire "/tmp", en utilisant la bibliothèque Unix. On compilera son programme par la commande
# ocamlc -o lstmp unix.cma lstmp.ml
(si votre programme est dans le fichier lstmp.ml).
let dir_handle = Unix.opendir "/tmp";;

try
while true do
let f = Unix.readdir dir_handle in
print_endline f
done
with End_of_file -> Unix.closedir dir_handle;;

Exercice 21. Ecrire un programme qui simule la commande printenv, c'est-à-dire qui affiche la liste des variables d'environnement et leur contenu sous la forme VAR=VALUE. Si des arguments sont passés sur la ligne de commande, seul le contenu des variables données est affiché. On utilisera la bibliothèque Unix.
let print_one acc_err var =
try print_endline (Unix.getenv var); acc_err
with Not_found -> true;; match Array.length Sys.argv with
n when n <= 1 ->
(* affiche tout l'environnement *)
Array.iter print_endline (Unix.environment ());
exit 0
| n ->
let args = Array.sub Sys.argv 1 (n - 1) in
let error = Array.fold_left print_one false args in
exit (if error then 1 else 0)
;;

On testera le programme avec les commandes suivantes.
Affichage de tout l'environnement :

# ./myprintenv

Affichage des variables SHELL et TERM :
# ./myprintenv SHELL TERM

Test d'erreur :
# ./myprintenv FOO ; echo $?

Chapitre 5  Programmation modulaire

La programmation modulaire est la séparation d'un programme en différents modules, définissant chacun des types de données et des fonctions pour traiter un aspect du programme. Par exemple, on peut définir un module pour le traitement des dates dans une application. Un module peut utiliser d'autres modules. Chaque module possède une implémentation et une interface.

L'intérêt de la séparation interface/implémentation est la possibilité de faire évoluer l'implémentation d'un module (correction, optimisation) de façon transparente pour les modules utilisant ce module, tant que l'interface reste compatible.

Une bibliothèque regroupe souvent plusieurs modules (comme la bibliothèque Labltk).

Voyons comment sont définis et utilisés les modules en OCaml.

5.1  Modules simples

5.1.1  Interface et implémentation

L'interface d'un module définit ce que le module offre, ce qui peut être utilisé depuis un autre module. L'implémentation du module doit définir les éléments présents dans l'interface mais peut en définir d'autres, qui ne seront alors visibles qu'à l'intérieur du module. L'interface d'un module est aussi appelée signature.

En OCaml, l'interface d'un module M est définie dans un fichier m.mli (ou M.mli) tandis que son implémentation est définie dans un fichier m.ml (ou M.ml). S'il n'y a pas de fichier d'interface correspondant à un fichier m.ml, alors le compilateur considère que tout ce qui est défini dans l'implémentation est visible dans l'interface, avec les types inférés par le compilateur.

On notera la relation entre le nom d'un module "racine" et le nom des fichiers définissant son interface et son implémentation.

Voyons un exemple d'implémentation dans un fichier m.ml :

type pair = int * int;;

let x = 1;;

let y = 2;;

let make_pair x y = (x,y);;

let first (x,_) = x;;

let second (_,y) = y;;

On peut définir une interface pour ce module dans le fichier m.mli :

type pair;;
val x : int;;
val make_pair : int -> int -> pair;;
val first : pair -> int;;
val second : pair -> int;;

On a ici rendu le type pair abstrait, car sa définition est masquée. On a également masqué la valeur y mais exposé x en indiquant son type. Enfin, on a exposé les fonctions first et second prenant en paramètre une valeur du type abstrait. Le type pair étant abstrait, nous sommes obligés d'utiliser la fonction make_pair pour créer des valeurs du type pair. Le passage d'un couple d'entiers (1,2) à la place d'une paire sera impossible en dehors du module car la définition du type étant masquée, pair et int * int sont considérés comme deux types non unifiables.

Pour ne pas abstraire un type de donnée, il faut redonner la même définition dans l'interface.

Lorsqu'un module ne contient que la définition de types de données, il n'est pas nécessaire d'avoir un fichier d'implémentation.

5.1.2  Compilation séparée

La compilation d'un module dont on a donné explicitement l'interface dans un fichier .mli et l'implémentation dans un fichier .ml commence par la compilation de l'interface :

# ocamlc -c m.mli
Un fichier m.cmi est produit, qui est l'interface compilée. Ensuite, l'implémentation est compilée à son tour :
# ocamlc -c m.ml

Un fichier .cmo de code-octet est produit. Dans le cas d'une compilation en code natif,
# ocamlopt -c m.ml

produit un fichier m.o contenant le code natif compilé et un fichier m.cmx contenant des informations supplémentaires nécessaires au compilateur OCaml lors de l'édition des liens.

Si un fichier m.mli existe mais pas de fichier m.cmi, alors le compilateur signale une erreur indiquant que l'inteface doit être compilée d'abord, puisque le code de l'implémentation doit être compatible avec l'interface.

S'il n'y a pas de fichier m.mli, la compilation de m.ml produit à la fois le fichier m.cmo (ou les fichiers m.cmx et m.o) et le fichier d'interface compilée m.cmi. Dans ce cas, tous les éléments de m.ml sont rendus visibles.

5.1.3  Sous-modules

Il est possible en OCaml de définir des modules à l'intérieur d'autres modules. Comme pour les valeurs et les types, les sous-modules peuvent apparaître ou non dans la signature du module qui les contient. Ils seront alors visibles ou non dans l'interface du module de plus haut niveau qui les contient.

Voyons un exemple, en définissant un module M contenant un module N. Nous définissons l'interface dans le fichier m.mli :

module N : sig
type t
val x : int
val foo : int -> t
end;;

et l'implémentation dans le fichier m.ml :

module N = struct
type t = int
let x = 1
let y = 2
let foo x = x + 1
end;;

Cependant, il est déjà possible de restreindre leur signature dans la partie implémentation, en imposant une contrainte de type dans l'implémentation :

# module N : sig
type t
val x : t
val foo : t -> t
end = struct
type t = int
let x = 1
let y = 2
let foo x = x + 1
end;;
module N : sig type t val x : t val foo : t -> t end

Ce type de contrainte permet de s'assurer par exemple que certains éléments du module N ne sont pas utilisés dans la suite du module conteneur M, puisque les restrictions dûes à l'interface de M ne s'appliquent que pour le code extérieur à M.

5.1.4  Types de modules

Pour les types de données, on peut noter :

val x : int * int;;
val y : (int * int) * (int * int);;

ou bien définir un type et l'utiliser par la suite dans les annotations de types :

type t = int * int;;
val x : t ;;
val y : t * t;;

De façon analogue, il est possible de définir des types de modules, c'est-à-dire de donner un nom à une signature :

module type Mon_type = sig
type t
val x : t
val foo : t -> t
end;;

et utiliser ensuite ce type de module comme signature :

# module N2 : Mon_type = struct
type t = int
let x = 1
let y = 2
let foo x = x + 1
end;;
module N2 : Mon_type

En allant plus loin, on peut définir des vues différentes sur un même module, par exemple :

module type Vue1 = sig
type t
val create : int -> t
end;;

module type Vue2 = sig
type t
val read : t -> int
end;;

# module Base = struct
type t = string
let create = string_of_int
let read = int_of_string
end;;
module Base :
sig type t = string val create : int -> string val read : string -> int end
# module M1 = (Base : Vue1 with type t = Base.t);;
module M1 : sig type t = Base.t val create : int -> t end
# module M2 = (Base : Vue2 with type t = Base.t);;
module M2 : sig type t = Base.t val read : t -> int end
# M2.read (M1.create 42);;
- : int = 42
# M1.read (M1.create 63);;
File "", line 1, characters 0-7:
Error: Unbound value M1.read

5.2  Foncteurs et réutilisabilité

«Les modules paramétrés» ou foncteurs «sont aux modules ce que les fonctions sont aux valeurs.» Il s'agit de modules prenant en paramètre un module et renvoyant un autre module. Le module retourné peut à son tour être un foncteur, tout comme une fonction "à plusieurs arguments" est une fonction prenant un paramètre et retournant une autre fonction.

Les foncteurs permettent d'écrire des modules s'abstrayant des structures de données manipulées, dont la représentation et les fonctions de manipulations sont fournies par un module en paramètre. Il est alors aisément possible d'appliquer un même algorithme à deux structures de données différentes, tant que le module en paramètre fournit les fonctions pour effectuer les opérations de base.

Voyons la définition et l'utilisation de foncteurs au travers d'un exemple. Nous souhaitons modéliser un guichet auquel est associé une file d'attente. La file d'attente sera abstraite, permettant la mise en place de différentes politiques de priorité, de façon transparente pour le code de gestion du guichet.

Nous commençons donc par définir le type de la file d'attente :

module type QueueType = sig
type 'a t
exception Empty
val create : unit -> 'a t
val pop : 'a t -> 'a
val push : 'a -> 'a t -> unit
end;;

Ensuite, nous définissons notre module de guichet, prenant en paramètre une file d'attente :

module Guichet = functor (Q : QueueType) -> struct
let create = Q.create
let add = Q.push
let handle_one f guichet =
try f (Q.pop guichet)
with Q.Empty -> ()
let rec handle_all f guichet =
match
try Some (Q.pop guichet)
with Q.Empty -> None
with
| None -> ()
| Some elt -> f elt; handle_all f guichet
end;;

Nous ne pouvons pas encore utiliser notre module, puisqu'il s'agit d'un foncteur :

# Guichet.create();;
File "", line 1, characters 0-14:
Error: Unbound value Guichet.create

Nous allons simuler notre guichet en utilisant d'abord une file FIFO. La signature demandée pour le module en paramètre est un sous-ensemble de la signature du module Queue , nous pouvons donc utiliser ce module comme modélisation de file d'attente. En appliquant ce module à notre foncteur, nous obtenons un guichet "premier arrivé, premier servi" :

# module Guichet_FIFO = Guichet(Queue);;
module Guichet_FIFO :
sig
val create : unit -> 'a Queue.t
val add : 'a -> 'a Queue.t -> unit
val handle_one : ('a -> unit) -> 'a Queue.t -> unit
val handle_all : ('a -> 'b) -> 'a Queue.t -> unit
end
# let fifo = Guichet_FIFO.create ();;
val fifo : '_a Queue.t = <abstr>
# List.iter (fun n -> Guichet_FIFO.add n fifo) [ 1 ; 2 ; 3 ; 4 ; 5 ];;
- : unit = ()
# Guichet_FIFO.handle_all (fun n -> print_int n; print_newline ()) fifo;;
1
2
3
4
5
- : unit = ()

Nous pouvons également modéliser la file d'attente par une pile; dans ce cas, le premier servi est le dernier arrivé :

# module Guichet_pile = Guichet(Stack);;
module Guichet_pile :
sig
val create : unit -> 'a Stack.t
val add : 'a -> 'a Stack.t -> unit
val handle_one : ('a -> unit) -> 'a Stack.t -> unit
val handle_all : ('a -> 'b) -> 'a Stack.t -> unit
end
# let pile = Guichet_pile.create ();;
val pile : '_a Stack.t = <abstr>
# List.iter (fun n -> Guichet_pile.add n pile) [ 1 ; 2 ; 3 ; 4 ; 5 ];;
- : unit = ()
# Guichet_pile.handle_all (fun n -> print_int n; print_newline ()) pile;;
5
4
3
2
1
- : unit = ()

Nous pouvons même faire un foncteur permettant de construire et tester un module de guichet, car les deux codes de tests ci-dessus sont les mêmes à la différence du guichet :

module Test (Q : QueueType) = struct
module G = Guichet(Q)
let guichet = G.create ()
let _ = List.iter (fun n -> G.add n guichet) [ 1 ; 2 ; 3 ; 4 ; 5 ]
let _ = G.handle_all (fun n -> print_int n; print_newline ()) guichet
end;;

module Foo = Test(Queue);;

module Foo = Test(Stack);;

On remarque que le compilateur impose correctement que les fonctions passées en paramètres aux fonctions handle_one et handle_all prennent en paramètres des valeurs du type des valeurs de la file d'attente :

# Guichet_FIFO.handle_one print_string fifo;;
File "", line 1, characters 37-41:
Error: This expression has type int Queue.t
but an expression was expected of type string Queue.t

On aurait pu masquer la représentation interne du guichet en utilisant une signature et en ajoutant un type de guichet :

module type GuichetAbstType = sig
type 'a t
val create : unit -> 'a t
val add : 'a -> 'a t -> unit
val handle_one : ('a -> unit) -> 'a t -> unit
val handle_all : ('a -> 'b) -> 'a t -> unit
end;;

# module GuichetAbst (Q : QueueType) : GuichetAbstType = struct
type 'a t = 'a Q.t
include Guichet(Q)
end;;
module GuichetAbst : functor (Q : QueueType) -> GuichetAbstType
# module Foo=GuichetAbst(Stack);;
module Foo :
sig
type 'a t = 'a GuichetAbst(Stack).t
val create : unit -> 'a t
val add : 'a -> 'a t -> unit
val handle_one : ('a -> unit) -> 'a t -> unit
val handle_all : ('a -> 'b) -> 'a t -> unit
end

On ne peut plus maintenant accéder à la représentation de la file depuis l'extérieur de la modélisation du guichet :

# Stack.pop (Foo.create ());;
File "", line 1, characters 10-25:
Error: This expression has type 'a Foo.t = 'a GuichetAbst(Stack).t
but an expression was expected of type 'b Stack.t

5.3  Déclarations locales de modules, modules anonymes

On peut déclarer localement un module, c'est-à-dire le construire en réduisant sa visibilité à une expression :

# let module Bar = struct let x = 1 end in Bar.x + 1;;
- : int = 2

Le module Bar n'est accessible que dans l'expression située après le in :

# Bar.x;;
File "", line 1, characters 0-5:
Error: Unbound module Bar

Il est également possible de construire des modules anonymes (sans nom). C'est souvent le cas lors de l'application d'un foncteur prenant un petit module en paramètre. Dans ce cas, on utilise directement la syntaxe struct ... end plutôt que la syntaxe module M = ... et l'utilisation de M dans la suite :

# module IntSet =
Set.Make (struct type t = int let compare = Pervasives.compare end);;
module IntSet :
sig
type elt = int
type t
val empty : t
val is_empty : t -> bool
val mem : elt -> t -> bool
val add : elt -> t -> t
val singleton : elt -> t
val remove : elt -> t -> t
val union : t -> t -> t
val inter : t -> t -> t
val diff : t -> t -> t
val compare : t -> t -> int
val equal : t -> t -> bool
val subset : t -> t -> bool
val iter : (elt -> unit) -> t -> unit
val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a
val for_all : (elt -> bool) -> t -> bool
val exists : (elt -> bool) -> t -> bool
val filter : (elt -> bool) -> t -> t
val partition : (elt -> bool) -> t -> t * t
val cardinal : t -> int
val elements : t -> elt list
val min_elt : t -> elt
val max_elt : t -> elt
val choose : t -> elt
val split : elt -> t -> t * bool * t
end

Chapitre 6  Environnement de développement

6.1  Les outils de la distribution

6.1.1  Interprète

L'interprète (toplevel) ocaml évalue au vol le code OCaml qui lui est passé. Il peut interpréter des fichiers passés sur la ligne de commande ou bien se comporter comme un shell et interpréter les phrases que l'utilisateur saisit.

6.1.2  Compilation

La distribution d'OCaml fournit différents outils destinés à la compilation, listés dans le tableau suivant :

ocamlc, ocamlc.optcompilateur vers du code-octet, en version code-octet ou en version native
ocamlopt, ocamlopt.optcompilateur vers du code natif, en version code-octet ou en version native
ocamlmklibgénération de bibliothèques contenant du code C et du code OCaml
ocamlbuildoutil de compilation automatisée (gérant notamment les dépendances)
 Documentation: http://brion.inria.fr/gallium/index.php/Ocamlbuild
ocamlcpfrontal à ocamlc permettant d'instrumenter le code pour utiliser le profileur

6.1.3  Débogage

La distribution OCaml fournit un débogueur, ocamldebug, permettant de suivre l'exécution d'un programme pas à pas, de revenir en arrière, d'inspecter des valeurs, mettre des points d'arrêt. Le programme doit être compilé en code-octet avec l'option -g.

La distribution inclut également un profileur, ocamlprof, capable d'afficher le nombre de passages à chaque point d'un programme compilé avec ocamlcp.

On pourra également utiliser l'outil GNU gprof par exemple pour afficher le temps passé dans chaque fonction.

6.1.4  Pré-processeurs

Les outils de pre-processing permettent de définir des extensions de syntaxe du langage afin par exemple de simplifier l'écriture de certaines constructions répétitives, de générer automatiquement du code pour chaque déclaration de type (comme des fonctions de lecture/écriture), ….

Deux outils de pre-processing sont disponibles pour OCaml :

6.1.5  Documentation

L'outil ocamldoc (ou ocamldoc.opt pour la version en code natif) fourni est l'équivalent de javadoc ou doxygen pour OCaml. Il permet de générer une documentation de référence en utilisant des commentaires formattés.

Ces commentaires sont des commentaires OCaml habituels, sauf qu'ils commencent par (** au lieu de (*. Ils se terminent comme les commentaires normaux par *). Le principe général est de placer un commentaire spécial juste au-dessus ou en-dessous de l'élément (valeur, type, module, …) commenté.

OCamldoc permet de générer des documentations aux formats LATEX, HTML, Texinfo, pages man. Il est également possible de définir son propre générateur et d'ajouter des éléments de syntaxe pour générer des documentations spécialisées.

6.1.6  Autres outils

Comme yacc et lex pour C, les outils ocamlyacc et ocamllex sont des générateurs d'analyseurs respectivement syntaxiques (parsers) et lexicaux (lexers). Le manuel OCaml donne un exemple concret de leur utilisation.

L'outil ocamldep analyse des fichiers sources OCaml et génère leurs dépendances dans un format compris par l'outil Make. Cela permet de ne pas avoir à indiquer explicitement les dépendances pour la compilation séparée des modules à l'aide d'un Makefile.

Enfin, ocamlbrowser permet la navigation dans les interfaces et implémentations, par exemple pour trouver facilement le type d'une fonction.

6.2  Editeurs, environnements, …

Avec le temps, plusieurs solutions d'édition de code OCaml ont vu le jour :

L'outil Findlib commence à s'imposer comme outil standard pour la gestion des bibliothèques installées : http://projects.camlcity.org/projects/findlib.html/.

D'autres outils sont listés dans la bosse Caml, dans le thème "Software development" et ses sous-thèmes :
http://caml.inria.fr/cgi-bin/hump.en.cgi?sort=0browse=52.

Chapitre 7  Conclusion

7.1  Conclusion

Au cours de cette formation, nous avons vu les concepts et constructions principales du langage Objective Caml, ainsi qu'une partie des bibliothèques fournies en standard.

Ce langage est bâti sur des bases théoriques solides avancées et propose plusieurs paradigmes de programmation (fonctionnelle, impérative, objets). Il possède un grand pouvoir d'expressivité, notamment grâce au polymorphisme et aux fonctions de 1er ordre.

«Les types structurés et les types abstraits permettent d'aborder les problèmes d'algorithmique et leurs structures de données complexes tout en s'abstrayant des problèmes de représentation mémoire et d'allocation.
Le modèle théorique fonctionnel sous-jacent au langage fournit une introduction précise aux notions d'évaluation et de typage dont << l'honnête programmeur >> se doit d'être instruit.
Les différents modèles de programmation peuvent être abordés indépendamment les uns des autres : de la structuration modulaire ou par objets des logiciels à la programmation système de bas niveau, il est peu de domaines où OCaml ne soit pas pertinent.
Son adéquation avec la programmation symbolique en fait un excellent support pour des enseignements théoriques comme la compilation ou l'intelligence artificielle. »

«Un des premiers sujets de satisfaction du développement en OCaml est son confort d'utilisation. Le compilateur se charge rapidement et son inférence statique de types ne laisse rien échapper. D'autres analyses statiques du code donnent de précieux indices d'anomalies sinon d'erreurs pour le programmeur : les filtrages incomplets sont signalés, l'application partielle d'une fonction dans une séquence est détectée, etc. À ce premier sujet de satisfaction s'en ajoute un second : le compilateur engendre très rapidement un code efficace.»

Plusieurs aspects du langage n'ont pas été abordés :

Nous n'avons pas abordé non plus certains aspects techniques :

Ces aspects pourraient être dévéloppés dans des formations ultérieures.

7.2  Pour aller plus loin

En plus des documentations indiquées dans la section 1.4, on pourra consulter les références suivantes pour continuer son apprentissage du langage et des concepts, dans différents domaines d'application :


Ce document a été traduit de LATEX par HEVEA