OCaml
Le langage


Cours Exercices
Le langage OCaml
  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. Modules
  12. Compilation séparée
  1. Mise en oeuvre
    1. Mode compilé
    2. Mode interprété
  2. Mode Emacs
  3. Petits exemples (1, 2, 3, 4)

  4. La commande cat
  5. La Calculette
  6. La commande wc

  7. Utilisation de la librairie Set
  8. Polynômes

Pour afficher correctement les lettres grecques utilisées dans les formules mathématiques de ce document, vous pouvez avoir besoin d'appliquer un script à votre installation de polices (pour Mozilla/Firefox sous Unix/X).

Avertissement

Le double but de ce cours est
  1. de vous faire découvrir le langage OCaml et de vous donner de bonnes bases pour que vous puissiez ensuite l'apprendre par vous-même,
  2. de vous donner quelques principes sous-jacents à la définition du langage OCaml (et des langages de programmation en général) qui vous permettant de mieux comprendre OCaml et plus généralement la notion de langage de programmation.
Ce cours est organisé en trois parties. Dans un premier temps, nous ferons un survol du langage OCaml, en deux étapes. Ensuite nous étudierons le typage, en prenant un sous-ensemble du langage OCaml comme exemple. Enfin, dans la dernière partie, nous verrons comment décrire l'évaluation des programmes, toujours avec un sous-ensemble de OCaml comme exemple. Nous ne pouvons bien sûr pas entrer dans les détails.

Apprentissage d'un langage

Se fait par la pratique: exercez-vous! y compris par couper/coller: n'ayez pas peur de regarder les solutions des exercices, de réutiliser du code.

Garder un pointeur sur
Il existe aussi quelques livres.

La petite histoire

Le langage Caml est le fruit de développements récents et continus pour améliorer l'expressivité et la sûreté de langages de programmation.
1978 Robin Milner propose ML comme un Méta-Langage pour décrire des stratégies dans un démonstrateur de théorèmes.
Il apparaît vite comme un langage de programmation à part entière.
1981 Premières implémentations de ML.
1985 Développement de Caml à l'INRIA, de Standard ML à Edimbourg, puis à Bell Labs (New Jersey, USA).
1990 Développement de Caml-Light par Xavier Leroy et Damien Doligez, à l'INRIA.
1995 Ajout d'un compilateur natif et d'un système de module.
1996 Ajout des objets.
2000 Ajout des arguments étiquetés et optionnels.

Domaines d'utilisation du langage OCaml

Langage d'usage général

Domaines de prédilection
Enseignement et recherche
Industrie

Quelques mots clés

Le langage OCaml est
Premiers pas en OCaml

Le mode compilé

Éditer le source
bienvenue.ml
print_string "Bienvenue !\n";;
Compiler et exécuter

% ocamlc -o bienvenue bienvenue.ml
% ./bienvenue
Bienvenue !

Le mode interactif


% ocaml Objective Caml version 2.06

# print_string "Bienvenue !\n";;
Bienvenue !
- : unit = ()

# let euro x = floor (100.0 *. x /. 6.55957) *. 0.01;;
val euro : float -> float = <fun>

# let baguette = 4.20;;
val baguette : float = 4.2

# euro baguette;;
- : float = 0.64

# exit 0;;

Utilisation du mode interactif

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

Le mode caml pour Emacs

Installation:

Insérer le contenu de
../emacs/emacs.el à la fin de votre fichier ~/.emacs.
À moins que cela ne soit déjà fait, télécharger et décompresser le mode emacs Tuareg. Installer les fichiers télécharges dans un répertoire appartenant à la valeur de la variable load-path.

Version interactive: au choix
  1. Ouvrir un fichier bienvenue.ml,
  2. Écrire les phrases dans ce fichier,
  3. Exécuter la commande Evaluate Phrase (ou Evaluate Buffer) dans le menu Tuareg.
Version compilée

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

- identificateur    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 e1; e2 : 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] <- c
tableaux [| 0; 1; 2; 3 |] t.(i)   t.(i) <- v
tuples (1, 2)   (1, 2, 3, 4) fst   snd

Un infixe entre parenthèses devient préfixe. Par exemple, ( + ) 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.

Les tuples sont hétérogènes, mais leur arité est fixée par leur type:

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 : string; valeur : float};;

let x = {nom = "euro"; valeur = 6.55957};;

x.valeur;;

Les champs peuvent être polymorphes

type 'a info = {nom : string; valeur : '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 : string; mutable â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 = <fun>

# 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.

On ne peut pas modifier la valeur d'une variable

Mais, on peut modifier la valeur des 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;;
En pratique, peu de valeurs ont besoin d'être ainsi encapsulées.

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();;


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 1   É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

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

Bien comprendre le noyau


Les fonctions sont des valeurs

OCaml est un langage fonctionnel. Cela signifie que les fonctions sont prises au sérieux

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

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

# let plus_deux = compose succ succ;;

# let symétrique f x y = f y x;;
val symétrique : ('a -> 'b -> 'c) -> 'b -> 'a -> 'c
Les fonctions peuvent être anonymes:

(fun x y -> x + y) 1 3
Deux notations équivalentes: let f x = e ou let f = fun x -> e

La puissance des fonctions

La fonction puissance

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

Quelques exemples

let plus u v = puissance ( succ ) u v;;
let fois u v = puissance (( + ) u) v 0;;
let puis u v = puissance (( * ) u) v 1;;

let marge = puissance (fun x -> "."^x);;
val marge : int -> string -> string

Les variantes

Les types concrets

type 'a résultat = Résultat of 'a | Rien;;
type carte = Carte of int | Valet | Dame | Roi;;
Les constructeurs sont toujours capitalisés.




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

type 'a liste = Vide | Cellule of 'a contenu
and 'a contenu = {tête : 'a; reste : 'a liste}

Filtrage

Les types concrets sont examiné par filtrage:

let valeur = fun x ->
match x with
Valet -> 11
| Dame -> 12
| Roi -> 13
| Carte 1 -> 20
| Carte x -> x
 Raccourcis:
let valeur x = 
match x with
Valet -> 11
| Dame -> 12
| Roi -> 13
| Carte 1 -> 20
| Carte x -> x

let valeur = function
Valet -> 11
| Dame -> 12
| Roi -> 13
| Carte 1 -> 20
| Carte x -> x

Les fonctions opérant sur un type récursif sont souvent récursives:

let rec longueur = function
Vide -> 0
| Cellule (tête, reste) -> 1 + longueur reste

Sémantique

Lorsqu'un argument est passé à un ensemble de clauses p1 -> e1 | ... | pn -> en
La définition est dite incomplète lorsque cela peut se produire. Lorsqu'un tel risque existe, un message d'avertissement est indiqué à la compilation.

Usage Il est préférable du moins dans un premier temps de ne pas écrire de définitions incomplètes.

Les motifs (patterns)

Ils peuvent

function Voila ({valeur = Carte x} as z, _) -> z.nom
Une variable ne peut pas être liée deux fois dans le même motif.


Exercice 2   Définir les listes à ``double entrée'' ayant un constructeur supplémentaire permettant d'ajouter un élément à la fin d'une autre liste.
Réponse
Écrire une fonction qui transforme une liste double en une liste simple (de la librairie).
Réponse




Exercice 3  [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
  2. Écrire la fonction pi : 'a list -> ('a -> int) -> int analogue mais en remplaçant la somme par le produit.
    Réponse
  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
  4. Réécrire la fonction sigma en utilisant une fonction de librairie.
    Réponse

Les exceptions

Exemple

exception Perdu;;

let rec cherche_la_clé k = function
(h, v) :: t -> if h = k then v else cherche_la_clé k t
| [] -> raise Perdu;;
let k =
try cherche_la_clé "Georges" [ ("Louis", 14); ("Georges", 5)]
with Perdu -> 10;;

Exercice 4   Ecrire un programme analogue sans utiliser d'exception.
Réponse



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éfinies
Exception Usage Gravité
Invalid_argument of unit     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


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 toute ambiguïté.

On utilise les exceptions pour

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.
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:
catstdin.ml
try 
while true do
print_string (read_line()); print_newline()
done
with End_of_file -> ();;

Exercice 5   É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



Exercices


Exercice 6  [La calculette]  

  1. Écrire une fonction sigma qui prend une fonction f, un tableau t et deux indices j et k, et qui calcule åi = jk f (t.(i))
  2. En déduire le calcul de la somme des éléments, des carrés ou la moyenne des éléments d'un sous-tableau.
  3. Produire un exécutable à partir de l'exercice précédent. Les arguments sont reçus sur la ligne de commande.
  4. 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




Exercice 7  [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

La trace

Fonctionne seulement en mode interactif à l'aide de directives (#commande [arguments])

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 interface conviviale pour Emacs: appeler la commande camldebug (faire ESC x camldebug nom-du-programme)

Les modules


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;;
: '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 f = identité in f 1, f true;;

L'abstraction
ne transmet pas le polymorphisme

(fun f -> f 1, f true) identité;; Erreur
est rejeté. Les deux occurrences de f 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;; Erreur
est également rejeté.

(Une application pourrait produire des effets de bords --- lecture et écriture --- avec des types différents)

Variables de type ``faibles''

Dans le mode interprété, on peut écrire:

# let f = identité identité;;
f : '_a -> '_a
Mais la fonction f n'est pas polymorphe. Les variables '_a ne sont pas des variables polymorphes. Elles sont dites variables de type faibles. Elles représentent un type particulier mais pas encore connu. L'expression

# f 1;;

est correcte, mais elle fige irréversiblement le type de f:

# f;;
f : int -> int
# f true;; => Erreur


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

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 (interfaces) entre les composantes
Rendre les gros programmes maintenables

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


Modules de base

Les structures sont collections de phrases

    struct p1 ...pn end

Les phrases sont celles du langage de base, plus
définition de sous-modules     module X = ...
définition de type de module     module type S = ...
Le nommage d'un module se fait à l'aide de la liaison module.

module S =
struct
type t = int
let x = 0
let f x = x+1
end

Utilisation d'un module

On fait référence aux composantes d'un module avec la notation pointée module.composante

... (S.f S.x : S.t) ...

Autre possibilité: la directive open module permet d'omettre le préfixe et le point:

open S
... (f x : t) ...

Modules emboîtés

Un module peut être composante d'un autre module:

module T =
struct
module R = struct let x = 0 end
let y = R.x + 1
end
La notation pointée et le open s'étendent naturellement et peuvent apparaître dans le corps d'autres modules:




module Q =
struct
let z = T.R.x
open T.R
...
end
  NB: La notation open T.R rend les composantes de T.R visibles dans la suite du corps de Q mais n'ajoute pas ces composantes à Q .

Les types des modules de base

Les signatures: collections de spécifications (de types).

    sig spécification ...spécification end

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
spécification de sous-modules module X : M
spécification de type de module module type S  [ = M]

Nommage d'un type de module: par la liaison module type

module type MA_SIGNATURE = sig ... end


Le système infère les signatures des modules (comme il infère les types des valeurs).



Module

module Exemple =
struct
type t = int
module R =
struct
let x = 0
end
let y = R.x + 1
end;;
 
Signature inférée

module Exemple :
sig
type t = int
module R :
sig
val x : int
end
val y : int
end

La construction (structure : signature)
    · vérifie que la structure satisfait la signature
(toutes les composantes spécifiées dans la signature doivent être définies dans la structure, avec des types au moins aussi généraux);
    · rend inaccessibles les parties de la structure qui ne sont pas spécifiées dans la signature;
    · produit un résultat qui peut être lié par module M = ...
Sucre syntaxique:
    module M : signature = structure
est équivalent à
    module M = (structure : signature)

Exemple de restriction

Écritures équivalentes




module S =
(struct
type t = int
let x = 1
let y = x + 1
end :
sig
type t
val y : t
end);;

 

module type SIG =
sig
type t
val y : t
end
module S : SIG =
struct
type t = int
let x = 1
let y = x + 1
end;;


S.x;; Erreur S.x is unbound
S.y + 1;; Erreur S.y is not of type int


Il est parfois important de distinguer des types isomorphes. Par exemples Euros et Dollars sont tous deux représentés par des flottants. Pourtant, il ne faut pas les confondre.

Ce sont deux espaces vectoriels, isomorphes mais disjoints, avec pour unités respectives l'euro et le dollar.




module Float =
struct
type t = float
let un = 1.0
let plus = (+.)
let prod = ( *. )
end;;
 

module type MONNAIE =
sig
type t
val un : t
val plus : t -> t -> t
val prod : float -> t -> t
end;;
La multiplication devient une opération externe sur les flottants.

Dans Float le type t est concret donc il peut être confondu avec "float". Par contre, il est abstrait dans les modules Euro et Dollar ci-dessous:

module Euro = (Float : MONNAIE);;
module Dollar = (Float : MONNAIE);;
Les types Euro.t et Dollar.t sont isomorphes mais incompatibles.

let euro x = Euro.prod x Euro.un;;
Euro.plus (euro 10.0) (euro 20.0);;
Euro.plus (euro 50.0) Dollar.un;; Erreur
Pas de duplication de code entre Euro et Dollar.

On peut donner une interface restreinte pour, dans un certain contexte, ne permettre que certaines opérations (typiquement, interdire la création de valeurs):




module type PLUS =
sig
type t
val plus : t -> t -> t
end;;
module Plus = (Euro : PLUS)
 

module type PLUS_Euro =
sig
type t = Euro.t
val plus : t -> t -> t
end;;
module Plus = (Euro : PLUS_Euro)
À gauche, le type Plus.t est incompatible avec Euro.t.

À droite, le type t est partiellement abstrait et compatible avec "Euro.t", et la vue Plus permet de manipuler les valeurs construites avec la vue Euro.

La notation with

La notation with permet d'ajouter des égalités de types dans une signature existante.

L'expression PLUS with type t = Euro.t est une abréviation pour la signature

sig
type t = Euro.t
val plus: t -> t -> t
end
On peut alors écrire

module Plus = (Euro : PLUS with type t = Euro.t);;
Plus.plus Euro.un Euro.un;;
Elle permet de créer facilement des signatures partiellement abstraites.

Modules et compilation séparée

Une unité de compilation A se compose de deux fichiers:

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
crée monprog

Le programme se comporte comme le code monolithique:

module A =
(struct contenu de a.ml end : sig contenu de a.mli end)

module B = struct contenu de b.ml end
L'ordre des définitions de modules correspond à l'ordre des fichiers objets .cmo sur la ligne de commande de l'éditeur de liens.

Modules paramétrés

Un foncteur est une fonction des modules dans les modules:

    functor (S : signature) -> module

Le module (corps du foncteur) est explicitement paramétré par le paramètre de module S. Il fait référence aux composantes de son paramètre avec la notation pointée.

module T = functor(S : SIG) ->
struct
type u = S.t * S.t
let y = S.g(S.x)
end

Application de foncteur

On ne peut pas directement accéder dans T. Il faut au préalable l'appliquer explicitement à une implémentation de la signature SIG (tout comme on applique une fonction ordinaire).


module T1 = T(S1)
module T2 = T(S2)
T1, T2 s'utilisent alors comme des structures ordinaires:

(T1.y : T2.u)
T1 et T2 partagent entièrement leur code.

Exemple (long) d'un compte bancaire


module type BANQUE = (* vue du banquier *)
sig
type t
type monnaie
val créer : unit -> t
val dépôt : t -> monnaie -> monnaie
val retrait : t -> monnaie -> monnaie
end;;

module type CLIENT = (* vue donnée au client *)
sig
type t
type monnaie
val dépôt : t -> monnaie -> monnaie
val retrait : t -> monnaie -> monnaie
end;;

Une modélisation simple de la banque

Sur le modèle des actions, le compte est donné au client... de façon abstraite bien sûr (sa représentation est cachée) afin que seule la banque puisse modifier le compte du client.

module Banque_au_porteur (M : MONNAIE) :
BANQUE with type monnaie = M.t =
struct
type monnaie = M.t
type t = { mutable solde : monnaie }
let zéro = M.prod 0.0 M.un and neg = M.prod (-1.0)

let créer() = { solde = zéro }
let dépôt c x =
if x > zéro then c.solde <- M.plus c.solde x; c.solde
let retrait c x =
if c.solde > x then dépôt c (neg x) else c.solde
end;;

module Poste = Banque_au_porteur (Euro);;

La modularité dans l'exemple

-- Les clients et le banquier ont des vues différents de la banque.

module Client : CLIENT
with type monnaie = Poste.monnaie
with type t = Poste.t
= Poste;;

let mon_ccp = Poste.créer ();;
Poste.dépôt mon_ccp (euro 100.0);;
Client.dépôt mon_ccp (euro 100.0);;
-- On peut créer des comptes dans différentes devises sans risque de confusion (elles seront détectées par le typage).

module Citybank = Banque_au_porteur (Dollar);;
let mon_compte_aux_US = Citybank.créer();;
Citybank.dépôt mon_ccp;; Erreur
Citybank.dépôt mon_compte_aux_US (euro 100.0);; Erreur

Modularité (suite)

-- On peut changer l'implémentation de la banque tout en en préservant l'interface.

Pour éviter la fraude, une banque donne seulement au client un numéro de compte et conserve l'état de son compte en interne.

La représentation d'un compte devient un simple numéro.

-- Plusieurs banques européennes ont alors des banques de données indépendantes (protégées).


module Banque_centrale = Banque_au_porteur (Euro);;
module Poste = Banque_au_porteur (Euro);;



Exercice 8  [Polynômes à une variable]  
  1. Réaliser une bibliothèque fournissant les opérations sur les polynômes à une variable. Les coefficients forment un anneau passé en argument à la bibliothèque.
    Réponse
  2. Utiliser la bibliothèque pour vérifier par exemple l'égalité (1 + X) (1 - X) = 1 - X2.
    Réponse
  3. Vérifier l'égalité (X + Y) (X -Y) = (X2 - Y2) en considérant les polynômes à deux variables comme polynôme en X à coefficients les polynôme en Y.
  4. Écrire un programme qui prend un polynôme sur la ligne de commande et l'évalue en chacun des points lus dans stdin (un entier par ligne). Le résultat est imprimé dans stdout.
    Réponse

Objets et classes

Pour mémoire. Voir le tutoriel




This document was translated from LATEX by HEVEA and HACHA.