Postscript réduit compressé(original compressé)
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

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 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
    · 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).
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
    · 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
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 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 = <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:

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

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

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
    · 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



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 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.
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é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:
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]  

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

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

        ... (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:
    · 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.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 tutorial




This document was translated from LATEX by HEVEA and HACHA.