( Le chapitre 1 de mon cours de caml à l'IIE est une version revue et corrigé des notes ci-dessous)

Introduction

Ocaml est un langage de la famille ML avec des traits fonctionnels et impératifs qui est enrichi par des constructions objets et un puissant système de modules. Il est développé à l'INRIA depuis 1995. Plusieurs raisons justifient le choix d'Ocaml pour l'apprentissage de la programmation objet:

Nous utilisons Ocaml plus particulièrement pour étudier la sémantique des langages objets, à savoir, le typage des constructions objets (et la manière de tirer profit des types), et le comportement des constructions objets.

Le mode interactif

Ocaml possède un mode interactif où il analyse et répond à chaque phrase entrée par l'utilisateur. Pour lancer Ocaml en mode interactif tapez (sur dirac ou pauli) la commande: ocaml. Vous aurez la réponse:

% ocaml
        Objective Caml version 2.01

#
Le caractère # invite l'utilisateur à entrer une phrase écrite dans la syntaxe Ocaml , phrase qui par exemple nomme une valeur, explicite une définition ou décrit un algorithme. Chaque phrase Ocaml doit terminer par ;; puis l'utilisateur valide sa frappe par un retour chariot. Dès lors, Ocaml analyse la phrase , calcule son type (inférence des types), la traduit en langage exécutable (compilation) et enfin l'exécute pour fournir la réponse demandée. En mode interactif, Ocaml donne systématiquement une réponse qui contient: Par exemple:
# let x = 6;;
val x : int = 6
La réponse de Caml signale que l'identificateur x est déclaré (val x), avec le type des entiers (:int) et la valeur 6 (=6).

On peut également effectuer des calculs ou définir des fonctions.

# x * 3;;
- : int = 18

# let double y = y*2;;
val double : int -> int = < fun >
Le type d'une fonction est noté t -> qt est le type des arguments et q est le type du résultat de la fonction. Dans le cas de la fonction double l'argument et le résultat sont de type entier.
# let z = double(x) ;;
val z : int = 12

# let rec factorielle n =
  if n<2 then 1  else n*factorielle (n-1);;  
val factorielle : int -> int = <fun>
# factorielle 5;;
- : int = 120

Enfin vous pouvez mettre des commentaires comme ceci:

# (* ceci est un premier commentaire *)
let unefonction x  = 
       (* ceci est un autres commentaire dans le corps de la fonction *)
       x+1;;      
val unefonction : int -> int = <fun>

Le noyau Ocaml

Ocaml est un langage qui possède un noyau fonctionnel enrichit par des traits objets et par un système de modules puissant. Nous décrivons dans cette partie quelques-unes des constructions du noyau Ocaml (en dehors des objets et des modules). La plupart d'entre elles font partie du langage Caml-Light, qui est une version réduite et simplifiée (sans objets et avec des modules plus simples) d'Ocaml. Une documentation en ligne pour Ocaml est disponible à l'adresse: http://pauillac.inria.fr/ocaml/ Une bibliographie plus complète est donnée sur http://caml.inria.fr/index-fra.html

Les valeurs

Les valeurs manipulées en Ocaml sont soit les objets, soit les valeurs de base (de type int, bool, string, etc.), soit des fonctions, soit des valeurs de types construits, pre-définis (listes, tableaux, etc) ou définis par l'utilisateur. En dehors des déclarations (des valeurs, des classes, des modules, des types) toute phrase Ocaml est considérée comme une expression et dénote ainsi une valeur résultat.

Les déclarations globales et locales

Un identificateur est déclaré globalement par le mot clef let:

# let x = 7;;
val x : int = 7

# x+2;;
- : int = 9
Un identificateur est déclaré localement par la construction let ...in:
#let y = 5 in x *y;
 - : int =45
Les identificateurs déclarées globalement ont une portée globale. Une déclaration locale est visible seulement dans l'expression qui suit le in:
#let y = 3 in x+y;;
- : int = 10
#y;;
Unbound value y

Les types

Les types de base

:

Opérateurs de comparaison (pour tous les types): =, >, <, >=, <=, <>

Voici quelques exemples:

# 1+ 2;;
- : int = 3
# 1.5 +. 2.3;;
- : float = 3.8
# let x = "cou" in x^x;;
- : string = "coucou"
# 2 > 7;;
- : bool = false
# "bonjour" > "bon";;
- : bool = true
- : bool = true
# "durand" < "martin";;
- : bool = true
# "ab" = "ba";;
- : bool = false

Les types construits predéfinis:

Quelques exemples:

# (1,true,"ab");;                               (* un 3-uplet *)
- : int * bool * string = 1, true, "ab"
# (2,3);;                                       (* une paire  *)
- : int * int = 2, 3
# fst (2,3);;                                     
- : int = 2
# snd (2,3);;
- : int = 3
# [1;2;3];;                                     (* une liste d'entiers *)
- : int list = [1; 2; 3]
# ["a";"bc"] @ ["bonjour"];;                    (* @: operateur de concatenation *)
- : string list = ["a"; "bc"; "bonjour"]
# [1]@[];;                                      (* []: liste vide  *)
- : int list = [1]
# List.length [1;2;3];;                         (* List: module de fonctions *)
- : int = 3                                     (* sur les listes            *)

# let a = [|1;2;3|];;             (* un tableau d'entiers *)
val a : int array = [|1; 2; 3|]
# a.(0) <- 50;;
- : unit = ()
# a;;
- : int array = [|50; 2; 3|]
# Array.length a;;               (* Array: module de fonctions pour tableaux *)
- : int = 3

Les types construits de l'utilisateur:

# type client = {numero: int; nom: string; solde: float};;
type client = { numero: int; nom: string; solde: float }

# let durand = { numero = 265; nom = "Durand"; solde=  0.0};;
val durand : client =  {numero=265; nom="Durand"; solde=0}
# durand.adresse;;
- : string = "rue Clovis"

# type couleur = Rouge | Vert | Bleu;;
type couleur = Rouge | Vert | Bleu

# type point_colore = { x: int; c: couleur};;
type point_colore = { x: int; c: couleur }

# let pc = {x = 1; c = Rouge};;
val pc : point_colore = {x=1; c=Rouge}

# type int_arbre = Vide | Noeud of int_arbre * int * int_arbre;;
type int_arbre = Vide | Noeud of int_arbre * int * int_arbre
# let un = Noeud(Vide, 1, Vide);;
val un : int_arbre = Noeud (Vide, 1, Vide)
On remarquera que la définition du type int_arbre est recursive.

Rouge, Vert et Bleu ou Vide et Noeud sont appelé des constructeurs de valeur ou simplement constructeur, ils permettent respectivement de construire toutes les valeurs de type couleur et int_arbre. En Ocaml les constructeurs commencent toujours par une majuscule.

Les fonctions

Comme une constante, une fonction se déclare avec un let. On a par exemple
# let carre x = x*x;;
val carre : int -> int = <fun>
On l'applique en la faisant suivre de son argument (éventuellement entre parenthèse)
# carre 9;;
- : int = 81

Il est possible de définir des fonctions sans leurs donner de non grâce au mot clef function ou fun

#  ( function x->x*x);;
- : int -> int = <fun>

#  ( function x->x*x) 5;;
- : int = 25

Une fonction anonyme est une valeur comme les autres qui peut être liée à un identificateur par un let. Les deux définition suivante sont équivalente

# let successeur1 = function x->x+1;;
val successeur1 : int -> int = <fun>
# let successeur2 x = x+1;;
val successeur2 : int -> int = <fun>

Une fonction peut prendre plusieurs arguments:

# let creer_pointcolore n col = {x = n; c = col};;
val creer_pointcolore : int -> couleur -> point_colore = < fun >
# let pc = creer_pointcolore 1 Rouge;;
val pc : point_colore = {x=1; c=Rouge}

#  let moyenne x y = (x+y)/2;;
val moyenne : int -> int -> int = <fun>

Lorsqu'une fonction a plusieurs arguments on n'est pas obligé de les lui fournir tous lors de l'application. Si on ne le fait pas le résultat de cette application partielle est lui même une fonction qui peut éventuellement être lié a un identificateur et être appliqué par la suite.

# let g = moyenne 2;;
val g : int -> int = <fun>
# g 6;;
- : int = 4
En fait la fonction moyenne peut donc être vue de deux façons, soit comme une fonction a deux argument entier renvoyant un entier, soit comme une fonction a un argument entier renvoyant une autre fonction des entiers vers les entiers.

De même l'argument d'une fonction peut lui même être une fonction:

# let f h = (h 1) +2;;
val f : (int -> int) -> int = <fun>
Comparez ce type avec celui de la fonction moyenne.

Enfin lorsqu'on déclare une fonction récursive il faut prévenir le compilateur en faisant suivre le mot clef let du mot clef rec.

# let  puissance b n =
        if n =0 then 1
                else b * puissance b (n-1);;

    Characters 63-73:
Unbound value puissance

# let rec puissance b n =
        if n =0 then 1
                else b * puissance b (n-1);;    
val puissance : int -> int -> int = <fun>
#   puissance 3 2;; 
- : int = 9

Polymorphisme

On a vue que le type des listes était t list ou t est le type des éléments de la liste, et qu'une liste d'entiers ne peut être employée avec un autre type. Mais quel sera le type de la liste vide ?. De même quel peut être le type de la fonction identité. interrogeons caml:
# [];;
- : 'a list = []
# let id x =x;;
val id : 'a -> 'a = <fun>
ici 'a désigne un paramètre de type, on le distingue syntaxiquement des types en le faisant précéder d'une apostrophe (').

Un identificateur dont le type contient des paramètres de type est dit polymorphe. On parle de polymorphisme paramétrique

Un paramètre de type peut être instancié par n'importe quel type:

# id 4;;
- : int = 4
# id "abc";;
- : string = "abc"
# id moyenne 3;;
- : int -> int = <fun>

Enfin un type peut contenir plusieurs paramètre de type:

# let f j x = j x;;
val f : ('a -> 'b) -> 'a -> 'b = <fun>
#  f succ;;
- : int -> int = <fun>

Remarquons que les opérateurs de comparaison = <> > < ... définis sur tous les types sont tous polymorphes:

# (=);;
- : 'a -> 'a -> bool = <fun>
# (>);;
- : 'a -> 'a -> bool = <fun>

Tests et alternative

Caml founit la construction classique if...then...else pour exprimer l'alternative.
# if 3>5 then "trois superieur a cinq" else "trois inferieur a cinq";;
- : string = "trois inferieur a cinq"
Pour respecter le typage, les expressions évalué dans chacun des cas doivent avoir le même type.
 if 3>5 then "trois superieur a cinq" else 5;;
Characters 43-44:
This expression has type int but is here used with type string

Le filtrage

Le filtrage de ML est un moyen de tester les cas de la structure d'un objet et de choisir des actions à effectuer selon chaque cas. La construction utilisée pour décrire la structure de chaque cas de l'objet est nommée filtre. La fonction suivante filtre ses arguments (une paire) pour faire leur addition en tenant compte du cas où l'un d'entre eux est zéro:

# let somme x y =
  match (x,y)
  with (0,n) -> n
  |    (n,0) -> n
  |    (a,b) -> a+b;;
val somme : int -> int -> int = < fun >
# somme 0 3;;
- : int = 3
# somme 2 4;;
- : int = 6

La fonction qui fait la somme des éléments dans une liste:

# let rec somme_liste l =
 match l
 with []  -> 0                             (* []  filtre de liste vide     *)
  |  a::r -> a + somme_liste r;;           (* a::r liste avec a en premier *)
val somme_liste : int list -> int = < fun >(* et r en reste                *)

# somme_liste [2;3;7];;
- : int = 12
La fonction qui fait la somme des éléments dans un arbre d'entiers (de type arbre_int défini plus haut).
# let rec somme_arbre arb =
    match arb
    with Vide  -> 0
     |  Noeud (a,n,b) -> somme_arbre a + n + somme_arbre b;;
val somme_arbre : int_arbre -> int = < fun >

somme_arbre (Noeud ((Noeud (Vide, 1, Vide)), 3, (Noeud (Vide, 5, Vide))));;
- : int = 9

les traits impératifs

Caml possède aussi les principales caractéristiques des langages impératifs ainsi que les boucle usuelles while et for.

Toutes les définitions que nous avons vu jusqu'à présent sont des constantes, le mot clef let ne fait pas d'affectation, il introduit un nouvel identificateur dans l'environnement. Vous pouvez redéfinir un identificateur en faisant un nouveaux let mais c'est un nouvel objet pas une modification de la valeur de l'ancien (qui est toujours là mais plus accessible).

La seule exception que nous avons vu est celle des tableaux.

Les champs des enregistrements peuvent également être modifiés par des affectations pourvu qu'ils aient été déclarés mutable lors de la définition de l'enregistrement.

# type point_mutable = {mutable x: float;mutable y: float};;
type point_mutable = { mutable x: float; mutable y: float }

# let translate p dx dy =
	p.x <-p.x +. dx; p.y <- p.y +. dy;;
val translate : point_mutable -> float -> float -> unit = <fun>

#let monpoint = {x=0.0;y=0.0};;
val monpoint : point_mutable = {x=0; y=0}

#translate monpoint 1.0 2.0;;
  - : unit = ()

# monpoint;;
- : point_mutable = {x=1; y=2}

De plus la bibliothèque standard fournit une notion de référence avec des opérateurs ! qui permet d'accéder au contenu de la référence et := qui permet de donner une nouvelle valeur a son contenu:

#let mavariable = ref 0;;
val mavariable : int ref = {contents=0}
# mavariable :=5;;
- : unit = ()
#  !mavariable;;
- : int = 5

Ces réferences sont définies en utilisant un enregistrement avec un seul champs mutable:

# type 'a ref = {mutable contents:'a};;
type 'a ref = { mutable contents: 'a }

#let ref x = {contents = x};;
val ref : 'a -> 'a ref = <fun>

# let (!) r = r.contents;;
  val ! : 'a ref -> 'a = <fun>

# let  (:=) r newval = r.contents <- newval;;
  val := : 'a ref -> 'a -> unit = <fun>

Exemples de Boucle:

#for i = 0 to 10 do print_int i done;;
012345678910- : unit = ()

# for i = 10 downto 0 do print_int i done;; 
109876543210- : unit = ()

#let  j =ref 10 in 
 while (!j)> 0 do begin print_int !j; j := !j-1; end done;;
10987654321- : unit = ()

# j;;
Characters 0-1:
Unbound value j
Notez que j est une variable locale à l'expresion suivant le in est n'est plus definie en dehors.

Les exceptions

Une exception est par définition un événement qui se produit rarement. Les exceptions permettent de traiter les situations qui empêchent l'accomplissement normal d'une action. Lever une exception, c'est signaler qu'une situation anormale est survenue, traiter une exception, c'est répondre à cette situation en exécutant les actions appropriées

Les exceptions sont déclarées par le mot clef exception. Attention en Ocaml le nom d'une exception doit commencer par une majuscule. Elles sont soulevées par la fonction raise et on les traitent avec la construction syntaxique try...with. Dans le bloc with on peut filtrer les différentes exceptions qui ont pu être soulevées.

Dans l'exemple ci dessous, on définit trois fonctions. Les deux premières qui peuvent chacune soulever une exception. La troisième dans laquelle les deux premières sont appelées montre comment ces exceptions peuvent être récupérées.

#exception ListeVide;;
exception ListeVide

#let tete l =
(* renvoie le premier element d'une liste *)
	match l with 
	[]->raise ListeVide
	|hd::tl->hd;;
val tete : 'a list -> 'a = <fun>

#tete [1;2];;
  - : int = 1

#tete [];;
Uncaught exception: ListeVide

#exception Negatif of int;;
exception Negatif of int

#let rec fact n =
 if n <0 then raise (Negatif n) else 
	if n=0 then 1 else n*fact(n-1);; 
 val fact : int -> int = <fun>

#let afficheFactPremier l =
     try 
        print_int (fact (tete l))
     with 
       ListeVide -> print_string "la liste est vide"
        |Negatif x  -> print_int x;print_string " est negatif"
        |_         -> print_string " Autre exception";;
val affichePremier : int list -> unit = <fun>

# afficheFactPremier [5;-3;6];;
120- : unit = ()
# afficheFactPremier [-5;-3;6];;
-5 est negatif- : unit = ()
# afficheFactPremier [];;           
la liste est vide- : unit = ()

Remarque: à la différence de Java les exceptions ne sont pas des objets, on n'a pas de hiérarchie des exceptions. La sélection de l'exception lors du traitement se fait par filtrage.

Exceptions et typage

. Les exceptions sont des valeurs à part entière du langage. Elles appartiennent au type prédéfini exn, on parle de valeurs exceptionnelles. Elle peut être manipulé comme toute les autre valeurs du langage. Le type exn est un type somme d'un genre particulier: sa définition n'est jamais achevé. On peut a tout moment lui ajouter de nouveaux constructeur (Comme ListeVide ou Negatif) grâce au mot clef exception.

Dans les exemples précédent on peut remarquer que le fait de lever une exception dans une fonction n'en contrarie pas le typage. Cela est possible par ce que la fonction raise utilisé pour lever une exception est une fonction polymorphe :

# exception ListeVide;;
exception ListeVide
# let toto = ListeVide;;
val toto : exn = ListeVide
# raise;;
- : exn -> 'a = <fun>

Les objets

Un objet en Ocaml est crée à partir d'une classe et composé de données et de mèthodes. Toute classe en Ocaml doit être nommée et paramétrée.

class suivant init =
        object
        val mutable x = init
        method succ = x <-x+1
        end;;          
class suivant : int -> object val mutable x : int method succ : unit end
       # 

class suivant init =
        object
        val  x = init
        method succ = x +1
        end;;            
class suivant : int -> object val x : int method succ : int end

Ocaml étant un langage typé, une classe a un type qui est calculé par le système. Les deux exemple précédent montre que le type d'une classe décrit les types du paramètre, des variables et des méthodes qui composent la classe.

Programmes indépendants

Tout les exemple que nous avons donné jusqu'ici étaient éxecuté d'une manière entièrement interactive. Le code Ocaml peut aussi être compilé et exécuté séparément. Voici l'exemple d'un programme qui revoie le nième élément de la suit de Fibonacci
(* File fib.ml *)
let rec fib n =
  if n < 2 then 1 else fib(n-1) + fib(n-2);;

let main () =
  let arg = int_of_string Sys.argv.(1) in
  print_int(fib arg);
  print_newline();
  exit 0;;
main ();;
Sys.argv est un tableau de strings contenant les paramètres de la ligne de commande. La fonction int_of_string convertie une chaîne de caractères en un entier.

Le programme peut être compilé et exécuté par les commandes suivantes:

$ ocamlc -o fib fib.ml
$ ./fib 10
89
$ ./fib 20
10946

Olivier Pons
1999-03-11