Bref survol du langage Caml-Light

Introduction

Il est d'usage de distinguer deux grandes familles de langages de programmation : les langages impératifs et les langages déclaratifs. Les langages impératifs comme Pascal, C, C++, et bien d'autres font correspondre à chaque instant de l'exécution du programme un état particulier qui se décrit en termes de contenu de la mémoire (données + instructions). L'exécution du programme se traduit donc par des changements successifs d'états et est pilotée par les structures de contrôle habituelles (la conditionnelle if, les boucles for, while, etc.). Les interfaces de ces langages masquent souvent un comportement voisin des langages d'assemblage, en ce sens qu'ils collent d'assez près à l'architecture des machines.

Un programme dans un langage déclaratif se décrit non pas en termes d'états de la mémoire, mais en fonction d'un modèle de calcul qui correspond à un schéma fonctionnel pour les uns (dont Caml Light, Lisp etc. ), à un schéma relationnel pour les autres (Prolog par exemple). Le but est tout à fait différent des langages impératifs : l'utilisateur décrit ses objectifs et non les moyens d'y parvenir. Leur intérêt réside dans le fait que l'utilisateur n'a pas à se soucier des structures de contrôle : elles sont naturellement engendrées par la description (beaucoup plus naturelle) du programme. Cette façon de voir la programmation assure une sûreté d'exécution plus grande (certain diront "au détriment de la vitesse d'exécution"). Nous allons voir que des algorithmes relativement complexes se codent trés rapidement et très facilement dans ce type de langages, d'autant plus s'ils sont issus de raisonnements ou de descriptions mathématiques. L'activité de programmation devenant quasiment une mathématique appliquée, avec les techniques démonstratives usuelles [notamment la récurrence].

Caml-Light est un langage de la famille ML devellopé à l'INRIA depuis 1984. C'est essentiellement un langage fonctionel, il possède donc un modèle de calcul basé essentiellement sur deux constructions : la définition et l'application de fonctions. Il possède par ailleurs quelques traits impératifs qui autorisent le novice à programmer à la Pascal en modifiant les données ou en faisant appel à des structures de contrôle, ce que nous déconseillons généralement. On peut presque voir Caml Light comme un "sur-langage" de Pascal en ce sens.

Caml Light est un langage qui déduit automatiquement le type des objets manipulés et interrompt la compilation en cas d'incohérence de type. On parle d'inférence de type. Il autorise la manipulation de types paramétrés (l'utilisateur peut définir par exemple des fonctions qui traitent les listes sans connaitre le type des objets de ces listes).

En Caml Light, le comportement des fonctions dépend de la nature des arguments : le filtrage d'argument évite de se préoccuper de la représentation interne des objets.

Toute fonction en Caml Light peut avoir un comportement exceptionnel sur des arguments erronées (par exemple, la division avec 0 en deuxième argument). La levée de telles exceptions est ensuite récupérable à n'importe quel niveau d'imbrication de l'appel, les programmes pouvant s'adapter à de tels comportements.

Caml Light dispose de nombreuses fonctionalités d'entrées/sorties, de traitement de flots de données permettant à l'utilisateur d'écrire assez facilement des langages de programmation.

Enfin, Caml Light fonctionne en deux modes qui le rendent particulièrement attractif : le toplevel, ou ligne de commande (tel un shell), ou en mode compilé (l'utilisateur écrit ses programmes dans des fichiers et développe ainsi ses applications).

Caml Light est une évolution essentiellement pour ordinateur de bureau de Caml qui fonctionnait à l'origine sur de gros ordinateurs. Le nom Caml provient de ML et CAM. ML signifie meta-language et CAM veut dire Categorical Abstract Machine.

Le mode interactif

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

% camllight
>       Caml Light version 0.73

#
Le caractère # invite l'utilisateur à entrer une phrase écrite dans la syntaxe Caml, phrase qui par exemple nomme une valeur, explicite une définition ou décrit un algorithme. Chaque phrase Caml doit terminer par ;; puis l'utilisateur valide sa frappe par un retour chariot. Dès lors, Caml 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, Caml donne systématiquement une réponse qui contient : Par exemple:
#let x = 6;;
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;;
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) ;;
z : int = 12

#let rec factorielle n =
  if n<2 then 1  else n*factorielle (n-1);; 
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;;      
unefonction : int -> int = <fun>

Les valeurs

Les valeurs manipulées en Caml sont soit les valeurs de base (de type int, bool, string, etc.), soit des fonctions, soit des valeurs de types construits, pré-définis (listes, tableaux, etc) ou définis par l'utilisateur. En dehors des déclarations (des valeurs,des types) toute phrase Caml 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;;
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és 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 *)
a : int vect = [|1; 2; 3|]
# a.(0) <- 50;;
- : unit = ()
# a;;
- : int vect = [|1; 2; 3|]
# vect_length a;;               (* fonctions longueur pour les  tableaux *)
- : int = 3

Les types construits de l'utilisateur:

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

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

# type couleur = Rouge | Vert | Bleu;;
Type couleur defined.

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

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

# type int_arbre = Vide | Noeud of int_arbre * int * int_arbre;;
Type int_arbre defined.

# let un = Noeud(Vide, 1, Vide);;
 un : int_arbre = Noeud (Vide, 1, Vide)
On remarquera que la définition du type int_arbre est récursive.

Rouge, Vert et Bleu ou Vide et Noeud sont appelé des constructeurs de valeur ou simplement constructeurs, ils permettent respectivement de construire toutes les valeurs de type couleur et int_arbre. En Caml 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;;
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 nom 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 équivalentes

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

Une fonction peut prendre plusieurs arguments :

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

#  let moyenne x y = (x+y)/2;;
moyenne : int -> int -> int = <fun>
(Pour les fonctions anonymes, la différence entre les mots clef function ou fun et que seul fun peut prendre plusieurs arguments).

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ée à un identificateur et être appliqué par la suite.

# let g = moyenne 2;;
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 deux arguments entiers renvoyant un entier, soit comme une fonction à 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;;
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);;    
puissance : int -> int -> int = <fun>
#   puissance 3 2;; 
- : int = 9

Polymorphisme

On a vu que le type des listes était t list 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;;
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;;
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:

# prefix =;;

- : 'a -> 'a -> bool = <fun>
# prefix >;;
- : '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ées 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;;
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 *)
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;;
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 boucles 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 unouvel identificateur dans l'environnement. Vous pouvez redéfinir un identificateur en faisant un nouveau 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 defined.

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

#let monpoint = {x=0.0;y=0.0};;
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 là son contenu :

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

Ces réferences peuvent être définies en utilisant un enregistrement avec un seul champ mutable:

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

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

# let deref  r = r.contents;;
deref : 'a ref -> 'a = <fun>

# let  affect r newval = r.contents <- newval;;
affect  : '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'expression suivant le in est n'est plus definie en dehors de cette expression .

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 Caml le nom d'une exception doit commencer par une majuscule. Elles sont soulevées par la fonction raise et on les traite 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;;
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);; 
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";;
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. Elles peuvent être manipulées comme toutes les autres valeurs du langage. Le type exn est un type somme d'un genre particulier : sa définition n'est jamais achevée. On peut à 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 parce que la fonction raise utilisé pour lever une exception est une fonction polymorphe :

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

Trace

Pour vous aider a "débuguer" ou simplement observer les appels de fonctions qui ont lieu pendant une évaluation, vous pouvez utiliser la commande trace :
#let rec fact n =
 if n <=1 then 1 else  n*fact(n-1);; 
fact : int -> int = <fun>
#trace "fact";;
The function fact is now traced.
- : unit = ()
#fact 3;;
fact <-- 3
fact <-- 2
fact <-- 1
fact <-- 0
fact --> 1
fact --> 1
fact --> 2
fact --> 6
- : int = 6

Programmes indépendants

Tous les exemples que nous avons donné jusqu'ici étaient éxecutés d'une manière entièrement interactive. Le code Caml 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 suite 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__command_line.(1) in
  print_int(fib arg);
  print_newline();
  exit 0;;
main ();;
sys__command_line 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:

$ caml c -o fib fib.ml
$ ./fib 10
89
$ ./fib 20
10946

Enfin caml dispose d'un système de modules permettant la compilation séparée et le développement de bibliotheque et de TDA. Nous les étudions séparément.
Olivier Pons
1999

Dernière modification le / Last modified : Fri Nov 19 15:42:08 MET 1999