Postscript, PDF Site Polytechnique Site INRIA
1 Initiation au langage Ocaml.


Cours Exercises
  1. Introduction
  2. Syntaxe
  3. Types et opérations élémentaires
  4. Enregistrements et references
  5. Entrées sorties

  6. Fonctions
  7. Variantes et filtrage
  8. Exceptions
  9. Mise au point

  10. Polymorphisme
  11. Programmation modulaire
  12. Compilation séparée
  1. Mise en oeuvre en mode compilé ou interprété
  2. Mode Emacs
  3. Makefile

  4. Petits exemples
    1. Commandes Unix (1, 2)
    2. Listes doubles
    3. Exceptions
    4. Calculs gelés
    5. Fonction sigma

  5. Jeu de cartes
  6. La calculette
  7. Comandes Unix cat et wc
  8. Streams


Avertissement

Cette introduction succincte au language Ocaml a pour but de vous permettre de vous débrouiller avec le langage Ocaml.

Ce n'est qu'un survol du language, vu sous l'angle de la pratique.
  1. Les exemples sont simples et classiques.
  2. Les constructions de bases sont présentées, mais avec très peu d'exemples.
  3. Les constructions particulières au langage Ocaml telles que l'utilisation des fonctions d'ordre supérieur, du filtrage ou du polymorphisme sont mériterait plus de considération et d'exercises.
Apprentissage d'un langage


Par la pratique exercez-vous!

Par couper/coller
    · Regarder les solutions d'exercises
    · Réutiliser et modifier le code déjà écrit.


En se référant à la définition du langage
    · la documentation: le manuel de référence, la librairie du noyau et les librairies standards.
    · le tutorial, l'expérience des autres (FAQ).
    · les librairies développées par les utilisateurs (voir aussi la page ocaml).


Profiter des informations on-line (voir page du cours)

Il existe aussi plusieurs livres sur Caml-Light ou Ocaml.

La petite histoire

Ocaml est le fruit de développements récents et continus:

    ·1975 Robin Milner propose ML comme méta-langage (langage de script) pour l'assistant de preuve LCF.

Il devient rapidement un langage de programmation à part entière
    ·1981 Premières implantations de ML
    ·1985 Développement de Caml à l'INRIA. et en parrallèle, de Standard ML à Edinburgh, de SML à New-Jersey, de Lazy ML à Chalmers, de Haskell à Glasgow, etc.

    ·1990Implamtation de Caml-Light par X. Leroy and D. Doligez
    ·1995 Compilateur vers du code natif + système de modules
    ·1996 Objets et classes (Ocaml)
    ·2000 Arguments étiquettés et optionnels, variants par J. Garrigue.


Domaines d'utilisation du langage Ocaml


Langage d'usage général

Domaines de prédilection
    · Calcul symbolique: Preuves mathématiques, compilation, interprétation, analyses de programmes.
    · Prototypage rapide.
    · Langage de script.
    · Programmation distribuée (bytecode rapide).


Enseignement et recherche
    · Classes préparatoires.
    · Utilisé dans de grandes universités (Europe, US, Japon, etc.).


Industrie
    · Startups, CEA, EDF, France Telecom, Simulog,...


Quelques mots clés

Le langage Ocaml est:

    ·fonctionnel: les fonctions sont des valeurs de première classe, elles peuvent être retournées en résultat et passées en argument à d'autres fonctions.
    ·à gestion mémoire automatique (comme JAVA)
    ·fortement typé: le typage garantit l'absence d'erreur (de la machine) à l'exécution.
    ·avec synthèse de types: les types sont facultatifs et synthétisés par le système.
    ·compilé ou interactif (mode interactif = grosse calculette)


De plus le language possède:

    ·une couche à objets sophistiquée,
    ·un système de modules très expressif.


2 Premiers pas en Ocaml.
Le mode compilé


Éditer le source
hello.ml
print_string "Hello World\n";;


Compiler et exécuter
ocamlc -o bienvenue bienvenue.ml
% ./bienvenue
Bienvenue !
Le mode interprété

ocaml
Objective Caml version 2.04
print_string "Bienvenue !\n";;
Bienvenue !
- : unit = ()
let euro x = floor (100.0 *. x /. 6.55957) *. 0.01;;
val euro : float -> float = 
let baguette = 4.20;;
val baguette : float = 4.2
euro baguette;;
- : float = 0.64
exit 0;;
Utilisation du mode interprété


Comme une grosse calculette

Pour la mise au point des programmes

Évaluation des phrases du programme en cours de construction (utilisation avec un éditeur)

Comme langage de script...

% ocaml < bienvenue.ml   équivalent à la version interactive
% ocaml bienvenue.ml   idem, mais suppression des messages
bonjour
#! /usr/bin/ocaml
for i = 1 to 3 do print_string "Bonne année! " done;;
print_newline();;
exit 0;;

chmod +x bonjour
bonjour
Le mode caml pour Emacs


Installation:

Insérer le contenu de  remy/emacs/ocaml.emacs à la fin de votre fichier ~/.emacs.

Version interactive: au choix
    · Appeler Ocaml par la commande run-caml
    · Ouvrir un fichier bienvenue.ml,

Écrire les phrases dans ce fichier,

Exécuter la commande eval-phrase dans le menu caml. (éventuellement, appeler start-subshell dans le menu caml.)


Version compilée Exécuter la commande compile dans le menu caml.

Pour pointer sur le source de l'erreur, se positionner sur le message d'erreur et presser la touche return.

Les programmes

Un programme est une suite de phrases:

-- définition de valeur      let x = e
-- définition de fonction      let f x ... xn = e
-- définition de fonctions      let [ rec ] f1 x1 ... = e1 ...
  mutuellement récursives        [ and fn xn ... = en]
-- définition de type(s)      type q1 = t1... [ and qn = tn ]
-- expression      e

Les phrases se terminent par ;; optionnel en général.

(* Cela est un commentaire (* et ceci un commentaire à 
 
 
 
l'intérieur d'un commentaire *) sur plusieurs lignes *)

Les expressions

-- définition locale    let x = e1 in e2
  (définition locale de fonctions mutuellement récursives)
-- fonction anonyme    fun x1 ... xn -> e
-- appel de fonction    f x1 ... xn
-- variable    x   (M.x si x est défini dans M)
-- valeur construite    (e1, e2)
  dont les constantes    1, 'c', "aa"
-- analyse par cas    match e with p1 -> e1 ... | pn -> en
-- boucle for    for i = e0 [down]to ef do e done
-- boucle while    while e0 do e done
-- conditionnelle    if e1 then e2 else e3
-- une séquence    e; e '
-- parenthèses    (e)   begin e end

Remarques

Il n'y a ni instructions ni procédures. Toutes les expressions retournent une valeur. La valeur unité () de type unit ne porte aucune information (unique valeur possible dans son type).

L'expression e dans les boucles for et while et dans la séquence doit être de type unit.

On peut ignorer le message d'avertissement, mais il est préférable de jeter explicitement le résultat inutile:

-- en utilisant une fonction
let ignore x = ();;
ignore : 'a -> unit
ignore e1e2 : unit
  -- en utilisant une définition:
let _ =  e1 in e2;;

Types, constantes et primitives de base
unit () pas d'opération!
bool true   false &&   ||   not
char 'a'   '\n'   '\097' code   chr
int 1   2   3 +   -   *   /   max_int
float 1.0   2.   3.14 +.   -.   *.   /.   cos
string "a\tb\010c\n" ^   s.[i]   s.[i]
Polymorphes
tableaux - c[| 0; 1; 2; 3 |] t.(i)   t.(i)
tuples (1, 2)   (1, 2, 3, 4) fst   snd


Un infix entre parenthèses devient préfixe. Par exemple, - v(+) x1 x2 est équivalent à x1 + x2

Tableaux

Les opérations sur les tableaux sont polymorphes: on peut créer des tableaux homogènes d'entiers, de booléens, etc.
[| 0; 1; 3 |] : int array
[| true; false |] : bool array
Les indices de tableaux varient de 0 à n-1 où n est la taille.

Les projections sont polymorphes: elles opèrent aussi bien sur des tableaux d'entiers que sur des tableaux de booléens.
fun x -> x.(0) : 'a array -> 'a
fun t k x = t.(k) <- x : 'a array -> int -> 'a -> unit
Les tableaux sont toujours initialisées:
Array.create : int -> 'a -> 'a array
Le type du tableau sera le type du second argument, qui sera utilisé pour initialiser toutes les cases du tableau.

Tuples

Ils sont hétérogènes, mais leur arité est fixée par leur type:
-- une paire (1, 2) : int * int
-- un triplet (1, 2, 3) : int * int * int
sont incompatibles.

Les projections sont polymorphes sur les tuples de même arité:
fun (x, y, z) -> x : ('a * 'b * 'c) -> 'a
Les paires ne sont qu'un cas particulier de tuple, pour lequel les deux projections fst et snd sont pré-définies.
fst : 'a * 'b -> 'a
snd : 'a * 'b -> 'b

Les fonctions
Les arguments sont passés sans parenthèses.
let plus x y = x + y : int -> int -> int
let plus (x, y) = x + y : int * int -> int
La seconde fonction a un seul argument qui est une paire.

L'application de fonction se fait par juxtaposition des arguments:
plus 1 3
Les fonctions peuvent être récursives:
let rec fact n = if n > 1 then n * fact (n -1) else 1;;
Et mutuellement récursives:
let rec ping n = if n > 0 then pong (n - 1) else "ping"
and pong n = if n > 0 then ping (n - 1) else "pong";;
Les enregistrements


Il faut les déclarer au préalable
type monnaie = {nom : stringvaleur : float}
let x = {nom = "euro"valeur =  6.55957}
x.valeur

Champs polymorphes
type 'a info = {nom : stringvaleur : 'a}
fun x -> x.valeur;;
- : 'a info -> 'a
L'étiquette nom désigne la projection de l'enregistrement 'a info, le dernier dans lequel elle a été définie.

(L'ancienne projection est devenue inaccessible)

Les structures mutables
Dans les définitions de type enregistrements, on peut déclarer des champs mutables.
    type personne = {nom : stringmutable âge : int}
permet de modifier la première composante, mais pas la seconde.
let p = {nom = "Paul"âge = 23};;
val p : personne = {nom="Paul"; âge=23}
let anniversaire x = x.âge <- x.âge + 1;;
val anniversaire : personne -> unit = 
p.nom <- "Clovis";;
Characters 0-17:
The label nom is not mutable
Les références
Le passage d'arguments se fait toujours par valeur.

Il n'existe pas de construction tels que &x ou *x en C qui retourne l'adresse à laquelle se trouve une valeur ou la valeur qui se trouve à une adresse.
La valeur d'une variable n'est pas modifiable
Mais, on peut modifier les champs d'un enregistrement
   type 'a boîte = { mutable contenu : 'a };;
En C
int x = 0;
x = x + 1;
En Ocaml
let x = {contenu = 1};;
x.contenu <- x.contenu +1;;
Raccourci:
let x = ref 1;;
x := !x +1;;
Peu de valeurs ont ainsi besoin d'être des références.

La ligne de commande


Les arguments passés à un exécutable

Ils sont placés dans le tableau Sys.argv : string array

Le nom de la commande est compté.

Exemple La commande Unix echo
echo.ml
for i = 1 to Array.length Sys.argv - 1
do print_string Sys.argv.(i); print_char ' ' done;
print_newline();;

Exercice 1  [Commade echo]   Corriger le programme ci-dessus pour que comme la commande Unix, pour reconnaître comme premier argument l'option "-n" qui qui a pour effet de ne pas imprime de fin de ligne.


Usage avancé La librairie Arg permet de lire ces arguments plus facilement.

Retourner un résultat


La fonction
exit : int -> unit

Elle arrête le programme et retourne au système d'exploitation la valeur entière reçue en argument (modulo 256).

Par défaut, une exécution retourne la valeur 0 si elle se termine normalement et une valeur non nulle autrement (une exception n'est pas attrapée par exemple)

L'usage est de retourner 0 pour une évaluation normale, et de réserver les valeurs non nulles pour décrire un comportement anormal.

Exercice 2  [Commandes true et false]   Écrire les commandes Unix true et false qui ne font rien la première retourne le code 0, la seconde le code 1.
Réponse
true.ml
exit 0;;
false.ml
exit 1;;
ocaml -c true true.ml
ocaml -c false false.ml

Les entrées-sorties

Canaux pré-définis
stdin : in_channel
stdout : out_channel
stderr : out_channel
  Ouverture de canaux
open_out : string -> out_channel
open_in : string -> in_channel
close_out : out_channel -> unit


Lecture sur stdin
read_line : unit -> string
read_int : unit -> int
  Écriture sur stdout
print_string : string -> unit
print_int : int -> unit


Les entrées sorties avancées
    · Voir la librairie noyau
    · Voir la librairie Printf par exemple, on écrira comme en C:
Printf.printf "%s année %d\n" "Année" 1999;;


Les fonctions


Les fonctions sont des valeurs

En particulier, elles peuvent être retournées en résultat et passées en argument à d'autres fonctions.

Les fonctions se manipulent comme en mathématiques.

La composition de deux fonctions est une fonction

let compose f g = fun x -> f (g x);;
val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = 

Fonctions anonymes

La fonction (fun x -> f (g x) est anonyme. Les définitions
let compose = fun f -> fun g -> fun x -> f (g x)
let compose f g x = f (g x);;
sont toutes équivalentes ainsi que leurs formes intermédiaires.

La puissance des fonctions


Fonction puissance
let rec power f n =
  if n <= 0 then (fun x -> x)
  else compose f (power f (n-1));;
val power : ('a -> 'a) -> int -> 'a -> 'a = 

Dérivation
let derivative dx f = fun x -> (f(x +. dx) -. f(x)) /. dx;;
val derivative : float -> (float -> float) -> float -> float = 

La puissance de la dérivée
let sin''' = (power (derivative 1e-5) 3) sin in sin''' pi;;
- : float = 0.999999
à la précision des calculs près...

Les variantes et le filtrage


Les définitions de types somme

Analogues aux définitions de types d'enregistrements. Les valeurs booléenes sont éléments d'un type énuméré:
type booleen = Vrai | Faux;;
let t = Vrai and f = Faux;;
Les constructeurs sont toujours capitalisés


Le filtrage permet d'examiner les valeurs d'un type somme.
let int_of_booleen x =
  match x with Vrai -> 0 | Faux -> 1;;

Racourci (notation équivalente)
let int_of_booleen = function Vrai -> 0 | Faux -> 1;;
Le type option

Les références sont toujours initialisées en Ocaml. Quelques fois il est nécessaire de créer des références qui seront définie plus tard. Ce traitement doit être explicite:
type 'a optionnel = Défini of 'a | Indéfini;;
let statut = Indéfini;;
Plus tard, on pourra définir la valeur:
statut := Défini (3);;
La lecture du statut doit aussi considéré le cas où celui-ci est indéfini.
match !statut with Définit x -> x | Indéfini -> 0;;;
Listes

Les listes sont simplement un type somme récursif

Types récursifs
type 'a liste =
  Vide
Cellule of 'a * 'a liste;;
  Les listes de la librairie

'a liste 'a list
Vide [ ]
Cellule (t,r) t :: r


Définition alternative (utilisant un enregistrement)
type 'a liste = Vide | Cellule of 'a cell
and 'a cell = { tête : 'areste : 'a liste };;

Listes modifiables en place (les champs sont mutables)
type 'a liste = Vide | Cellule of 'a cell
and 'a cell = { mutable hd : 'amutable tl : 'a liste };;
Le jeu de carte

Il combine types somme et types enregistrement:
type carte = Carte of ordinaire | Joker
and ordinaire = { couleur : couleurfigure : figure; }
and couleur = Coeur | Carreau | Pic | Trèfle
and figure =  As | Roi | Reine | Valet | Simple of int;;
Définition de fonctions de construction auxiliaires:
let valet_de_pic = Carte { figure = Valetcouleur = Pic; };;
val valet_de_pic : carte = Carte {couleur=Pic; figure=Valet}
let carte n s = Carte {figure = ncouleur = s}
let roi s = carte Roi s;;
val carte : figure -> couleur -> carte = 
val roi : couleur -> carte = 
Filtrage en profondeur

let valeur = function
  | Carte { figure = As } -> 14
  | Carte { figure = Roi } -> 13
  | Carte { figure = Reine } -> 12
  | Carte { figure = Valet } -> 11
  | Carte { figure = Simple k } -> k
  | Joker -> 0;;
Un motif peut capturer plusieurs cas.
let petite = function
  | Carte { figure = Simple _ } -> true
  | _ -> false;;
Sémantique du filtrage

Lorsqu'un argument est passé à un ensemble de clauses
p1 -> e1 | ... | pn -> en
    · la première clause qui filtre l'argument est exécutée, les autres sont ignorées.
    · si aucune clause ne filtre l'argument, une exception est levée.
La définition est dite incomplète lorsqu'il existe une expression qui n'est filtrée par aucun motif. Dans ce cas, un message d'avertissement est indiqué à la compilation.
let simple = function Carte {figure = Simple k} -> k;;
Characters 13-52:
Warning: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
Joker
val simple : carte -> int = 

Usage Il est préférable d'éviter les définitions incomplètes.
Les motifs (patterns)

Ils peuvent
    · être emboîtés, contenir des produits et des sommes
    · ne pas énumérer tous les champs des enregistrements
    · lier une valeur intermédiaire avec mot clé as
    · n'imposer aucune contrainte (représentés par le caractère _)
Une variable ne peut pas être liée deux fois dans le même motif.

function Carte ({ figure = Simple _ } as z) -> z.couleur;;
function Carte z, z -> true;;
Exercices

Exercice 3  [Listes à double entrée]   Définir le type double des listes à ``double entrée'', ie. ayant un constructeur supplémentaire permettant d'ajouter un élément en fin de liste.
Réponse
type 'a double =
    Gauche of 'a * 'a double | Droite of 'a double * 'a | Vide;;
let d = Droite (Gauche (1, Droite (Gauche (2, Vide), 3)), 4);;
Écrire une fonction list_of_double qui transforme une liste double en une liste simple (de la librairie).
Réponse
let rec dédouble l = function
  | Vide -> l
  | Gauche (h,t) -> h::dédouble l t
  | Droite (th) -> dédouble (h::lt;;
        
let list_of_double l = dédouble [] l;;
list_of_double d;;

Exercice 4  [Jeu de cartes]   Deux cartes sont incompatibles si aucune n'est le Joker et si elles sont différentes. Elles sont compatibles sinon. Un ensemble de cartes dont toutes les paires de cartes sont compatibles est compatible. Les ensemble de cartes sont representé par des listes, mais considérés à permutation près.

Écrire une fonction
compatibles qui prend un ensemble de cartes et retourne l'ensemble de toutes les sous-parties compatibles les plus larges possibles.
Réponse
La seule difficulté provient de la carte Joker, qui peut remplacer n'importe quelle autre carte. Un joker est compatibles avec deux cartes qui ne sont pas compatibles entre elles. La relation de compatibilité n'est donc pas une relation d'équivalence.

On définit la relation asymétrique binaire meilleure: la carte Joker est meilleure que la carte Roi, mais pas l'inverse.
let meilleure x y = 
  match xy with
    Carte uCarte v -> u.figure = v.figure
  | _Joker -> true
  | JokerCarte _ -> false 
let pas_meilleure x y = not (meilleure y x);;
La fonction compatibles est définie pas induction. Si la main est vide, l'ensemble des parties compatibles est vide. Si le premier élément de la main est un joker, alors les parties compatibles sont celles du reste de la main dans chacune desquelles il faut ajouter un jojer. Sinon, le premier élément est une carte ordinaire: les parties compatibles sont d'une part l'ensemble des cartes compatibles avec celle-ci, et l'ensemble des parties compatibles avec les cartes qui ne sont pas compatibles avec celle-ci.
let rec compatibles = function
  | Joker :: t ->
      List.map (fun p -> Joker::p) (compatibles t)
  | h :: t ->
      let found = h :: List.find_all (meilleure ht in
      let rest = compatibles (List.find_all (pas_meilleure htin
      found :: rest
  | [] -> [];;
Voici un exemple de recherche:
compatibles
 [ roi TrèfleJokerroi CarreauJokerroi Coeur;
   roi Piccarte Reine Trèflecarte Reine Picvalet_de_pic ];;
- : carte list list =
[[Carte {couleur=Trèfle; figure=Roi}; Joker;
  Carte {couleur=Carreau; figure=Roi}; Joker;
  Carte {couleur=Coeur; figure=Roi}; Carte {couleur=Pic; figure=Roi}];
 [Joker; Joker; Carte {couleur=Trèfle; figure=Reine};
  Carte {couleur=Pic; figure=Reine}];
 [Joker; Joker; Carte {couleur=Pic; figure=Valet}]]
En déduire une fonction testant si une main contient un carré.
Réponse
let carré main =
  List.exists (fun x -> List.length x >= 4) (compatibles main);;
Exercice 5  [La fonction Sigma]  
  1. Écrire une fonction sigma : ('a -> int) -> 'a list -> int telle que l'expression sigma f l calcule la formule åx Î l f(x).
    Réponse
    let rec sigma f = function
      | [] -> 0  
      | x :: rest -> f x + sigma f rest;;
  2. Écrire la fonction pi : 'a list -> ('a -> int) -> int analogue mais en remplaçant la somme par le produit.
    Réponse
    let rec pi f = function
      | [] -> 1
      | x :: rest -> f x * pi f rest;;
  3. Comment généraliser la fonction en une fonction
    fold : (int -> int -> int) -> 
     
     
     
     
     
     
     
    int -> 'a list -> ('a -> int) ->  int
    pour ne pas dupliquer le code? Donner deux versions de fold, d'abord une seule fonction globale récursives, puis utiliser une fonction locale récursive auxilliaire en lui passant le minimum d'argument(s).
    Réponse
    let rec fold (oper : int -> int -> int) (x0 : intf = function 
      | [] -> x0
      | x :: rest -> oper (f x) (fold oper x0 f rest);;
    let fold (oper : int -> int -> int) (x0 : intf = 
      let rec aux = function
        | [] -> x0
        | x :: rest -> oper (f x) (aux restin
      aux;;
    let sigma f l = fold ( + ) 0 f l;;
    let pi f l = fold ( * ) 1 f l;;


  4. Réécrire la fonction sigma en utilisant une fonction de librairie.
    Réponse
    let sigma f l = List.fold_right (fun x r -> (f x) + rl 0;;
Les exceptions


Exemple
exception Perdu
let rec cherche_la_clé k = function
    (hv) :: t -> if h = k then v else cherche_la_clé k t
  | [] -> raise Perdu
        
let k =
  try cherche_la_clé "Louis" [ "Georges", 14; "Louis", 5;]
  with Perdu -> 10;;
Exercice 6  [Exceptions]   Ecrire un programme analogue sans utiliser d'exception.
Réponse
type 'a valeur = Trouve of 'a | Perdu
let rec cherche_la_clé f = function
    (hk) :: t -> if f h then Trouve k else cherche f t
  | [] -> Perdu
let k =
  match cherche_la_clé "Georges" [ "Louis", 14; "Georges", 5;] with
  | Trouve x -> x
  | Perdu -> 10


Syntaxe

-- Définition (phrase) exception C [ of t ]
-- Levée (expression) raise e
-- Filtrage (expression) try e with p1 -> e1 ... | pn -> en

Remarquer l'analogie avec la construction de filtrage.

Exceptions pré-définis
Exception Usage gravité
Invalid_argument of string accès en dehors des bornes forte
Failure of string liste vide, retourne le nom de la fonction moyenne
Not_found of unit fonctions de recherche nulle
Match_failure of ... Échec de filtrage fatale
End_of_file Fin de fichier nulle

Sémantique
    · Le type exn est un type somme.
    · La levée d'une exception arête l'évaluation en cours et retourne une valeur ``exceptionnelle'' (du type exn).
    · Une exception ne peut être éventuellement rattrapée que si elle a été protégée par une expression try e1 with m
    · si l'évaluation de e1 retourne une valeur normale celle-ci est retournée sans passer par le filtre.
    · si l'évaluation de e1 retourne une valeur exceptionnelle, alors celle-ci est passée au filtre m. Si une des clauses pi -> ei filtre l'exception, alors ei est évaluée, sinon l'exception n'est pas capturée, et la valeur exceptionnelle est propagée.


    · On peut observer une exception (ie. l'attraper et la relancer):
  try f x with Failure s as x -> prerr_string sraise x


Usage des exceptions

Il est préférable de définir de nouvelles exceptions plutôt que de réutiliser les exceptions pré-définies afin d'éviter tout ambiguïté.

On utilise les exceptions pour

    · reporter une erreur et interrompre le calcul en cours;

Par exemple, l'exception Match_failure est levée à l'exécution lorsqu'une valeur n'est pas filtrée. Elle indique le nom du fichier et la position filtre en cause.

    · communiquer une valeur de retour rare ou particulière;

Par exemple, les exceptions Not_found et End_of_file sont utilisées pour programmer et leur levées sont tout à fait normales.

Exemple : la commande Unix cat

Une version restreinte:
try 
  while true
  do print_string (read_line()); print_newline() done
with End_of_file -> ();;
Exercice 7  [La commande cat]   Écrire la version complète, qui concatène l'ensemble des fichiers dont les noms sont passés en argument ou bien reproduit le flux d'entrée lorsque la commande est appelée sans argument.
Réponse
let echo chan = 
  try while true do print_string (input_line chan); print_newline() done
  with End_of_file -> ();;

if Array.length Sys.argv <= 1 then echo stdin
else
  for i = 1 to Array.length Sys.argv - 1 
  do
    let chan = open_in Sys.argv.(iin
    echo chan;
    close_in chan
  done;;

Exercises
Exercice 8  [La calculette]  

- Écrire une fonction sigma qui prend une fonction f, un tableau et un indice et qui calcule åi = jk f (t.(i))

- En déduire le calcul de la somme des éléments, des carrés ou la moyenne des éléments d'un sous-tableau.

- Faire un exécutable à partir de l'exercise précédent. Les arguments sont reçus sur la ligne de commande.

- Par défaut le programme calcule la somme. Si le premier argument est la chaîne
-carre ou -moyenne, alors il calcule la somme ou la moyenne des éléments restants.
Réponse
let sigma f t j k =
  let y = ref 0 in
  for i = j to k do y := !y + f t.(idone;
  !y;;

let k = Array.length Sys.argv - 1;;
match Sys.argv.(1) with
"-carre" ->
    let f x = let n = int_of_string x in n * n in
    print_int (sigma f Sys.argv 2 k)
"-moyenne" ->
    let r = sigma int_of_string Sys.argv 2 k in
    print_float (float r /. float (Array.length Sys.argv - 2))
_ ->
    print_int (sigma int_of_string Sys.argv 1 k)
;;
print_newline();;
ocamlc -o calculette calculette.ml
calculette -carre 1 2 3 4 5 6 
91

Exercice 9  [La commande Unix wc]   compte l'ensemble des caractères, mots et lignes de chacun des fichiers passés en arguments ainsi que les totaux respectifs et les imprimes dans la sortie standard.
Réponse
type counts = {
 mutable chars : int;
 mutable lines : int;
 mutable words : int;
};;

let wc_count = {chars = 0; lines = 0; words = 0};;
let wc_count_total = {chars = 0; lines = 0; words = 0};;

let reset_count () =
 wc_count.chars <- 0;
 wc_count.lines <- 0;
 wc_count.words <- 0;;

let cumulate () =
 wc_count_total.chars <- wc_count_total.chars + wc_count.chars;
 wc_count_total.lines <- wc_count_total.lines + wc_count.lines;
 wc_count_total.words <- wc_count_total.words + wc_count.words;;

let rec counter ic iw =
 let c = input_char ic in
 wc_count.chars <- wc_count.chars + 1;
 match c with
 | ' ' | '\t' ->
    if iw then wc_count.words <- wc_count.words + 1 else ();
    counter ic false
 | '\n' ->
    wc_count.lines <- wc_count.lines + 1;
    if iw then wc_count.words <- wc_count.words + 1 else ();
    counter ic false
 | c ->
    counter ic true;;

let count_channel ic =
 reset_count ();
 try counter ic false with
 | End_of_file -> cumulate (); close_in ic;;

let output_results s wc =
  Printf.printf "%7d%8d%9d %s\n" wc.lines  wc.words wc.chars  s
;;

let count_file file_name =
 try
  let ic = open_in file_name in
  count_channel ic;
  output_results file_name wc_count;
 with Sys_error s -> print_string sprint_newline(); exit 2
;;

let main () =
  let nb_files = Array.length Sys.argv - 1 in
  if nb_files > 0 then
    begin 
      for i = 1 to nb_files do
        count_file Sys.argv.(i)
      done;
      if nb_files > 1 then output_results "total" wc_count_total;
    end
  else
    begin
      count_channel stdin;
      output_results "" wc_count;
    end;
  exit 0;;

main();;
La trace

Fonctionne seulement en mode interprété à l'aide de directives

Mise en oeuvre
#trace nom-de-fonction;;
#untrace nom-de-fonction;;
#untrace_all;;

Changer l'imprimeur par défaut La trace se combine souvent avec l'ajout d'imprimeurs
#install_printer imprimeur;;
#remove_printer imprimeur;;
L'argument imprimeur est le nom d'une fonction d'impression de type t -> unit. Les valeurs du types t sont alors imprimées avec le nouvelle fonction.

Le débogueur

Fonctionne seulement en mode compilé.

Permet de faire du pas en pas en avant et en arrière, de visualiser l'état de la pile et les valeurs des variables locales.

Mise en oeuvre

Compiler avec ocamlc -g

Appeler ocamldebug nom-du-programme

Il existe une interfAs conviviale pour Emacs: appeler la commande camldebug (faire ESC x camldebug nom-du-programme)

Polymorphisme

Le typeur infère les types d'un programme. Il existe souvent plusieurs solutions:
let identité x = x;;
  : int -> int 
  : string -> string
  : 'a * 'a -> 'a * 'a
  : 'a -> 'a
Le typeur retourne le type le plus général (il en existe toujours un si le programme est typable).
let identité x = x;;
val identité : 'a -> 'a = 
Les autres se retrouvent par instantiation des variables de type. On dit que l'identité est polymorphe.

Support du polymorphisme


La construction de liaison introduit le polymorphisme.
let apply f x = f x in apply succ 1, apply print_int 1;;

L'abstraction ne transmet pas le polymorphisme
(fun apply -> apply succ 1, apply print_int 1) (fun f x -> f x);;
est rejeté. Les deux occurrences de apply doivent avoir le même type.

(Il n'existerait plus toujours de type le plus général)

L'application interrompt également le polymorphisme
let f = identité identité in f 1, f true;;
est également rejeté.

(Une application pourrait produire des effets de bords --- lecture et écriture --- avec des types différents)
Variables de type ``faibles''

let r = ref [];;
val r : '_a list ref = {contents=[]}
La valeur r n'est pas polymorphe (c'est une application).

Les variables de type '_a ne sont pas des variables polymorphes. Elles sont dites faibles; elles représentent un type particulier mais pas encore connu.

La première contrainte fixera irréversiblement le type de f:
r := [1]; r;;
- : int list ref = {contents=[1]}
r := [true];;
Characters 0-1:
This expression has type int list ref but is here used with type
  bool list ref
Les variables peuvent de retrouver de types faible involontairement:
let map_apply = List.map (fun (f,x) -> f x);;
val map_apply : (('_a -> '_b) * '_a) list -> '_b list = 
perdant ainsi le polymorphisme.

Pour conserver le polymorphisme, il suffit de retarder la spécialisationde la fonction:
let map_apply l = List.map (fun (f,x) -> f xl;;
val map_apply : (('a -> 'b) * 'a) list -> 'b list = 
Calculs gelés

L'application d'une fonction à un argument effectue le calcul immédiatement.

Il est parfois souhaitable de retarder le calcul
    · Pour éviter un gros calcul inutile.
    · Pour retarder ou répéter les effets de bord associé au calcul.


On peut contrôler le moment d'exécution des calculs à l'aide de l'abstraction et de l'application.
let plus_tard f x = fun () -> f x;;
let un = plus_tard print_int 3;;
let mark = plus_tard print_string "*";;
mark(); un(); mark();;
*3*- : unit = ()
Exercice 10  [Gelé les calculs]   On considère le programme:
let ifzéro n sioui sinon = if n = 0 then sioui else sinon;;
ifzéro 3 (print_string "oui") (print_string "non");;
Expliquer l'anomalie.

Corriger cette anomalie en redéfinissant la fonction
ifzéro avec le type int -> (unit -> 'a) -> (unit -> 'a) -> 'a. On donnera le programme corrigé complet.
Réponse
La fonction ifzéro évalue ses deux arguments (de plus l'ordre d'évaluation n'est pas déterminé).

Il faudrait que ses arguments soient systématiquement gelés, donc passés comme des fonctions de type unit -> 'a.
let ifzéro n yes no = if n = 0 then yes() else no();;
ifzéro 3 (fun() -> print_string "oui") (fun() -> print_string "non");;

Streams

Exercice 11  [Calculs paresseux]   Un stream est une liste potentiellement infinie. Elle doit bien sûr être représentée de façon finie, les calculs des s'effectuant paresseusement au fur et à mesure des besoin. On peut le représenter par le type concret suivant:
type 'a stream = 'a status ref
and 'a status = 
 
 
 
 
Nil
 
 
| 
Cons of 'a * 'a stream
 
 
| 
Exception of exn
 
 
| 
Frozen of (unit -> ('a * 'a stream));;
Un stream dont le statut est une excepetion est un stream dont l'évaluatinon à lancé (et donc doit relancer cette exception).

Définir les fonction
hd, tl et nth sur les streams qui retourne le premier élément, le reste du stream, et le nième élément.
Réponse
let hd x =
  match !x with
  | Nil -> failwith "hd"
  | Cons (h,t) -> h
  | Exception x -> raise x
  | Frozen f ->
      try let h,t = f() in x := Cons (ht); h
      with exn -> x := Exception exnraise exn;;

let tl x =
  match !x with
  | Nil -> failwith "tl"
  | Cons (h,t) -> t
  | Exception x -> raise x
  | Frozen f ->
      try let h,t = f() in x := Cons (ht); t
      with exn -> x := Exception exnraise exn;;

let rec nth l n =
  if n < 0 then failwith "nth"
  else if n = 0 then hd l
  else nth (tl l) (pred n);;
Donnez également des fonctions de construction nil, cons : 'a -> 'a stream -> 'a stream et une fonction freeze : (unit -> ('a * 'a stream)) -> 'a stream, qui retourne le stream vide, ajoute un élément explicite en tête d'un stream et construit un stream à partir d'une fonction génératrice.
Réponse
let nil = ref Nil
let cons x t : 'a stream = ref (Cons (xt))
let freeze f : 'a stream = ref (Frozen f);;
Construire le stream des entiers pairs.
Réponse
let entiers_pair = let rec s n = freeze (fun () -> ns (n + 2)) in s 0;;
nth entiers_pair 3;;
Définir une fonction filter : ('a -> bool) -> 'a stream -> 'a stream qui prend un prédicat un stream et retourne le stream des éléments qui satisfont les prédicat. En déduire le stream des entiers pairs à partir du stream des entiers.
Réponse
let rec filter f s =
  let rec first s =
    let ht = hd stl s in
    if f h then hfilter f t else first t in
  freeze (fun () -> first s);;
let entiers = let rec s n = freeze (fun () -> ns (n + 1)) in s 0;;
let entiers_pairs = filter (fun x -> x mod 2 = 0) entiers;;
nth entiers 3;;
nth entiers_pairs 3;;

La programmation traditionnelle

Les programmes sont écrits de façon linéaire.

On peut seulement ajouter de nouvelles définitions à la fin du programme.

Réutilisation du code par ``couper-coller''.

Pas de partage de code
    · duplication du code, et des erreurs!
    · difficulté de maintenance.


La programmation modulaire

Découpage du programme en morceaux compilables indépendamment
Rendre les gros programmes compilables

Donner de la structure au programme
Rendre les gros programmes compréhensibles

Spécifier les liens (interfAss) entre les composantes
Rendre les gros programmes maintenables

Identifier des sous-composantes indépendantes
Rendre les composantes réutilisables

Deux approches de la programmation modulaire


Programmation par objects

Les objects peuvent sont définis à partir de classes.

Les classes peuvent être construites à partir d'autres classes par héritage.

Un langage de module très riche

Les modules de bases permttent de contrôler la visibilité des champs en cachant certains définitions de valeurs ou de types.

Les functors sont des fonctions des modules vers les modules, permettant de générer de nouveaux modules à partir d'autres.

Les modules sont également le support du mécanisme de compilarion séparée.

Modules et compilation séparée

Une unité de compilation A se compose de deux fichiers:
    · Le fichier d'implémentation A.ml:
une suite de phrases.
    · Le fichier d'interface A.mli (optionnel):
une suite de spécifications.
Une autre unité de compilation B peut faire référence à A comme si c'était une structure, en utilisant la notation pointée A.x ou bien en faisant open A.

Les spécifications sont (essentiellement):
spécification de valeurs val x : s
spécification de types abstraits type t
spécification de types manifestes type t = t
spécification d'exceptions exception E

Compilation séparée d'un programme

Fichiers sources: a.ml, a.mli, b.ml

Étapes de compilation:

ocamlc -c a.mli compile l'interface de A crée a.cmi
ocamlc -c a.ml compile l'implémentation de A crée a.cmo
ocamlc -c b.ml compile l'implémentation de B crée b.cmo
ocamlc -o monprog a.cmo b.cmo   édition de liens finale

L'ordre des définitions de modules correspond à l'ordre des fichiers objets .cmo sur la ligne de commande de l'éditeur de liens.

Utilisation de la commande make


Le modèle
    · Copier Makefile dans votre dossier de travail.
    · Éditer les variables SOURCES et EXEC

(au plus un exécutable à la fois)
    · Faire make
    · Au premier appel, si un message indique l'absence d'un fichier .depend, alors est nécessaire de faire touch .depend.


Mode graphique Même scénario, mais il faut en plus modifier règler les variables suivantes
    LIBS=$(GRAPHICS)
    CUSTOM=-custom
(pour l'ajout d'autres librairies on procédera de façon similaire, comme décrit dans le fichier Makefile)

Vous pouvez bien sûr utiliser votre propre Makefile.

Pour en savoir plus

Suivez les cours de majeure 1:
    · Langages de programmation.
    · Modularité, Objets et types.
Suivez aussi les cours de majeure 2....


This document was translated from LATEX by HEVEA.