Avertissement
Le double but de ce cours est
-
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,
-
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 ferrons 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 entrés 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.
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
· |
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):
Un gestionnaire mémoire récupère les données qui ne sont plus accessibles;
l'utilisateur n'a plus à s'en préoccuper.
|
· |
fortement typé (comme JAVA): Le typage garantit l'absence d'erreur (de
la machine) à l'exécution.
|
· |
avec synthèse de types: Les types sont facultatifs car ils sont
synthétisés par le système.
|
· |
compilé ou interactif
|
Le mode compilé
Éditer le source
print_string "Bienvenue !\n";;
|
|
Compiler et exécuter
% ocamlc -o bienvenue bienvenue.ml
% ./bienvenue
Bienvenue !
|
|
% 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 = <fun>
# 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 |
Le mode caml pour Emacs
Installation:
Insérer le contenu de ../emacs/emacs.el à 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 .
|
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 e1; e2 : unit
|
|
|
-- en utilisant une définition:
|
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 infix 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:
-- une paire (1, 2) :
int * int, et
-- 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:
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
|
Champs 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
|
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
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.
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 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:
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 peut_être = Voilà of 'a | Pas_encore;;
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
|
|
|
Raccourci:
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 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 alors levé.
|
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
· |
ê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 _ )
|
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.
Écrire une fonction qui transforme une liste double en une liste simple (de
la librairie).
Exercice 3 [La fonction Sigma]
-
Écrire une fonction
sigma : ('a -> int) -> 'a list -> int
telle que
l'expression sigma f l
calcule la formule åx Î l
f(x).
-
Écrire la fonction
pi : 'a list -> ('a -> int) -> int
analogue mais en remplaçant la somme par le produit.
-
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éécrire la fonction sigma en utilisant une fonction de librairie.
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.
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 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
· |
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é au filtre m. Si une des clause des pi -> ei filtre
l'exception, alors ei est évaluée, sinon l'exception n'est pas
capturée, et la valeur exception est retournée (propagée).
|
|
· |
On peut observer une exception (l'attraper et la relancer):
try f x with Failure s as x -> prerr_string s; raise 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 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.
Exercices
Exercice 6 [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'exercice 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.
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.
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.
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)
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 b
n'est pas polymorphe. Les variables '_a
ne sont pas des variables polymorphes. Elles sont dites variables de type
faible. Elles représentent un type particulier mais pas encore
connu. L'expression
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
· |
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 (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
Autre possibilité: la directive open module permet d'omettre
le préfixe et le point:
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:
· | Le fichier d'implémentation A.ml: une suite de phrases semblable à l'intérieur de struct...end
|
· | Le fichier d'interface A.mli (optionnel): une suite de spécifications semblable à l'intérieur de sig...end
|
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.
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 |
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 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]
-
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.
-
Utiliser la bibliothèque pour vérifier par exemple l'égalité (1 + X) (1 -
X) = 1 - X2.
-
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.
-
É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
.
Objets et classes
Pour mémoire. Voir le tutorial
This document was translated from LATEX by
HEVEA and HACHA.