( Le chapitre 1 de mon cours de caml à l'IIE est une version revue et corrigé des notes ci-dessous)
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:
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:
# let x = 6;; val x : int = 6La 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 -> q
où t 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>
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 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.
Un identificateur est déclaré globalement par le mot clef let:
# let x = 7;; val x : int = 7 # x+2;; - : int = 9Un identificateur est déclaré localement par la construction let ...in:
#let y = 5 in x *y; - : int =45Les 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
^
&
, or, not
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
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
# 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.
# 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 = 4En 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
# [];; - : '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>
# 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 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 = 12La 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
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 jNotez que j est une variable locale à l'expresion suivant le in est n'est plus definie en dehors.
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.
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>
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.
(* 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