Vous êtes sur la page 1sur 62

Cours de Programmation 2

Programmation à moyenne et large échelle

Notes de cours essentiellement basées sur celles de Ralf Treinen,


modifiées par Roberto Di Cosmo

1. Programmation modulaire
2. Programmation orientée objet
3. Programmation concurrente, distribuée
4. Les programmes dans leur environnement : l’approche UNIX
Langage de programmation : Objective Caml

Programmation à petite échelle


(programming in the small)
– programmation structurée : structuration de la sémantique du programme par la
syntaxe (procédures, boucles while au lieu des goto)
– Sémantique dénotationnelle
– Programmer avec des invariants (logique de Hoare)

Le problème de la programmation à large échelle


(programming in the large)
Projets de programmation importants :
– ≥ 106 lignes de code
– travail en groupe (la composition du groupe peut changer, tous les membres ne
sont pas au même endroit, etc.)
– modifications du programme au long de son temps de vie
– conception, programmation, inspection, tests, documentation par des équipes
différentes
The art of programming is the art of organizing complexity
(E. Dijkstra)
⇒ Découpage en « morceaux », mais pas n’importe comment :
– Découpage logique correspondant à la logique interne du projet
– Compilation séparée1
– Faciliter la maintenance
– Faciliter les extensions du programme
– Réutilisation du code (bibliothèques)
1 La compilation de OpenOffice prend plus de 24h

1
Plan du chapitre Modules
1. Modules comme unités de compilations
2. Encapsulation et types abstraits
3. Analyse descendante
4. Le langage des modules
5. Modules paramétrés
6. Encapsulation et valeurs mutables (⇒objets)

1 Modules comme unités de compilation


Un module est une unité de programme qui regroupe des définitions (définitions de
quoi ? ça dépend du langage).
– OCaml : types, valeurs (aussi de type fonctionnel), exceptions
– Pascal : constantes, variables, fonctions, procédures, etc.
Première approche : Module = Unité de compilation.
Le module B exporte quelque chose (par ex. une fonction), le module A l’importe.
Relation entre modules : Dépendance. Module A dépend du module B ssi A utilise
un nom défini par B.

Dépendances entre modules


Module M1 Module M2
type t = fonction f (· · ·)
. .
. .
. .

Module M3
variable x : t
.
.
.
fonction g (· · ·)
.
.
.
f (· · ·)
.
.
.

Modules
Si et compilation
module A dépend séparée
du module B, alors la compilation de A a besoin de connaître les
définitions effectuées par B :
– soit textuellement (le compilateur regarde dans le source de B)
– soit en forme compilée (cas normal) ⇒ compiler B avant de compiler A.
Dans le deuxième cas, la compilation d’un module A engendre
– un « résumé » des définitions effectuée par A
– du code, avec des adresses symboliques pour les identificateurs définis dans des
autres modules.

2
À la fin : assemblage des morceaux de code et résolution des symboles (édition des
liens, angl. : linking).
La relation de dépendance doit être acyclique.2

Un graphe de dépendances

Module 1 Module 2

Module 3 Module 4

Programme principal

Dans quel ordre peut-on compiler ces modules ? Réponse, on utilise un tri topolo-
gique, qui est à la base d’outils comme make.

Interface et corps d’un module


– Interface : Résumé des définitions d’un module
– Corps (Implantation) : Code réalisant les définitions
Possibilités pour organiser un module en interface et corps :
– Turbo Pascal : dans un seul fichier.
– OCaml : séparé dans un fichier interface .mli et un fichier corps .ml. Avan-
tage : Si A dépend de B, alors on fixe d’abord l’interface de B. Puis, on peut
rédiger (et même compiler) les corps de A et B indépendamment.

Exemple d’un module en Turbo Pascal


UNIT aut1;

INTERFACE

type states = (q0, q1, q2);


symbols = ’a’..’b’;

var
initialstate : states;
finalstates : set of states;
transition : array [states,symbols] of states;
2 pour l’instant, voir mixins

3
procedure autinit();

(suite du fichier)

IMPLEMENTATION

procedure autinit;
begin
initialstate := q0;

finalstates := [q0,q1];

transition[q0,’a’] := q1;
transition[q0,’b’] := q0;
end;

END.

Exemple d’un module en OCaml


Fichier Interface aut1.mli
(* Module for the automaton ... *)

type states = Q0 | Q1 | Q2
type symbols = char

val initialstate: states


val finalstates: states list
val transition: states * symbols -> states

exception Transition_undefined

Fichier Corps aut1.ml


type states = Q0 | Q1 | Q2
type symbols = char
exception Transition_undefined

let initialstate = Q0
let finalstates = [Q0; Q1]
let transition = function
Q0, ’a’ -> Q1
| Q0, ’b’ -> Q1
| _ -> raise Transition_undefined

4
Importation dans les modules de OCaml
Deux constructions :
1. Directive open B au début du module importeur A : fait accessible en A toutes
les définitions exportées par B.
Avantage : plus court.
2. Préfixer les noms par le nom du module : B.f dénote l’identificateur f exportée
par le module B.
Avantage : plus explicite, et pas d’ambiguïté (plusieurs module peuvent exporter
le même identificateur).

Dépendance d’une interface d’un module d’un autre module


L’interface d’un module A peut dépendre d’un module B, c’est précisément le cas
quand A exporte un type concret dont la définition utilise un type exporté par B.
Exemple : Le module Expressions exporte un type expr, et l’interface du
module Instruction contient
type instr =
Print of Expression.expr
| Affect of string * Expression.expr
| While of Expression.expr *instr list

Compilation des modules en OCaml


– ocamlc module.mli produit module.cmi à partir de module.mli
– ocamlc -c module.ml produit module.cmo à partir de module.ml et
module.cmi
– ocamlc -o program module_1.cmo module_2.cmo . . . module_n.cmo
fait l’édition des liens et crée l’exécutable program.
Il ne faut pas que module_i dépend de module_j pour i < j.

2 Encapsulation
(et plus sur la compilation séparée)
On peut toujours obtenir une interface d’un module OCaml en compilant le corps
avec l’option -i :
% ocamlc -i -c aut1.ml
type states = Q0 | Q1 | Q2
and symbols = char
exception Transition_undefined
val initialstate : states
val finalstates : states list
val transition : states * char -> states

5
Pourtant, cette interface n’est pas satisfaisante pour deux raisons :
– Pas de commentaire (l’interface doit contenir une spécification des valeurs etc.
définies sous forme de commentaire).
– Souvent on ne veut pas exporter toutes les définitions d’un module. Il n’y a pas
d’encapsulation.

2.1 Le principe d’encapsulation


Exporter aussi peu de définitions que possible : l’interface d’un module peut être
plus abstraite que son corps. On cache les fonctions, types, exceptions auxiliaires. En
OCaml
– Une interface peut exporter un type abstrait : L’interface ne contient que la dé-
claration type : t, et le corps contient sa définition complète : type t =
...
– Si un type concret est exporté par l’interface, alors le corps doit contenir la même
définition.
– Tout identificateur (ou exception) exporté doit être défini par le corps, et cela
avec un type égal ou plus général, éventuellement en utilisant les définitions des
types abstraits.
Le corps peut contenir des types, fonctions, exceptions privés (pas exportés).

2.2 Intérêt de l’encapsulation


– L’interface comme « contrat » entre programmeur d’un module et son utilisa-
teur : liberté de réalisation au programmeur.
L’interface contient toutes les informations qui sont nécessaires pour utiliser le
module (avec des commentaires ! ! !).
– Le codage d’un module peut être changé sans que cette modification ne soit
visible vers l’extérieur.
– Types abstrait : maintenance d’un invariant puisque modifications que par des
fonctions exportées.
L’encapsulation dans un module s’ajoute à la possibilité d’encapsulation par défi-
nition locale (à une fonction) d’un identificateur.

2.3 Exemple
Un module pour des tours d’entier (des piles d’entiers, où la valeur décroit vers le
sommet, comme pour les Tours de Hanoï).
On ne veut permettre que la construction des tours qui satisfont l’invariant : Les
valeurs décroissent vers le sommet.
Solution : Déclarer un type abstrait, et ne permettre la modification d’un tour que
par une fonction exportée par le module.

(* interface of the module for towers of integers. A tower is a stack of


integers which are strictly decreasing from bottom to top. *)

6
type tower
exception Operation_illegal
(* the empty tower *)
val empty: tower
(* (push i t) returns a new tower consisting of t with additionally i at
the top, provided that result is still a tower. Otherwise raise
Operation_illegal.*)
val push: int -> tower -> tower
(* (pop t) returns a tower which consists of t without its top element.
raises Operation_illegal when t is empty. *)
val pop: tower -> tower
(* (top t) returns the top element of t, raises Operation_illegal when
t is empty. *)
val top: tower -> int

(* implementation of module tour *)


type tower = int list
exception Operation_illegal

let empty = []
let top = function
h::r -> h
| [] -> raise Operation_illegal
(* (can_be_pushed i t) is true iff i can be pushed on t *)
let can_be_pushed i = function
[] -> true
| h::r -> i<h
let push i t =
if can_be_pushed i t then i::t else raise Operation_illegal
let pop = function
h::r -> r
| [] -> raise Operation_illegal

Programme principal
open Tour

let a = empty;;
let b = pop (push 17 (push 42a));;
print_int (top b);;
print_newline ();;

ou, équivalent

let a = Tour.empty;;

7
let b = Tour.pop (Tour.push 17 (Tour.push 42a));;
print_int (Tour.top b);;
print_newline ();;

Un Makefile pour compiler le tout


# Édition des liens et création de l’exécutable
main: tour.cmo main.cmo
ocamlc -o main tour.cmo main.cmo

# Compilation du corps du module tour


tour.cmo: tour.ml tour.cmi
ocamlc -c tour.ml

# Compilation de l’interface du module tour


tour.cmi: tour.mli
ocamlc tour.mli

# Compilation du corps du module main


main.cmo: main.ml tour.cmi
ocamlc -c main.ml

Calculer automatiquement les dépendances


% ocamldep *.mli *.ml
main.cmo: tour.cmi
main.cmx: tour.cmx
tour.cmo: tour.cmi
tour.cmx: tour.cmi

Un Makefile générique
.SUFFIXES: .ml .mli .cmo .cmi .cmx
OBJECTS = tour.cmo main.cmo

main: $(OBJECTS)
ocamlc -o main $(OBJECTS)

.ml.cmo:
ocamlc -c $<

.mli.cmi:
ocamlc $<

include .depend

8
depend:
ocamldep *.mli *.ml > .depend

clean:
-rm *.cmo *.cmi main

Pour plus d’info sur make : en emacs, taper C ONTROL - H I, puis M MAKE

3 Analyse descendante (top down)


(ou : Comment trouver le bon découpage en modules ?)
Analyse du plus général vers le plus particulier.
Commencer avec le programme entier : quelle est l’entrée, quelle est le résultat ?
Puis, couper le fonctionnement du programme en sous-tâches. Identifier les fonc-
tionnalités principales de la sous-tâche (⇒ fonctions exportées), les types de données
et des fonctionnalités partagés par les sous-tâches (⇒ modules auxiliaires utilisés par
des autres modules).
Il faut déjà avoir une idée grossière de l’implémentation des modules.

Exemple
Un interpréteur pour un langage de programmation avec déclaration des variables
types (par ex. PASCAL).
Fonctionnalité principale : lit un programme, déclenche une erreur si le programme
n’est pas correcte, sinon l’exécute et affiche le résultat.
Découpage en sous-tâches :
1. Analyse syntaxique
2. Vérification des types et déclarations des variables
3. Exécution
Les trois modules travaillent sur la syntaxe abstraite des programmes.

9
Premier graphe de dépendance

programme principal

analyse syntaxique typage exécution

syntaxe abstraite

Interface du module Syntaxe


La syntaxe abstraite est un type inductif, pas de raison de cacher sa définition.
type typ = Entier | Réel | Bool
type expr = Var of string
| EConst of int
| Plus of expr * expr
...
type instr = Affect of string * expr
| While of expr * instr list
...
type decl = string * typ
type prog = decl list * instr list

Interface du module typage


val bien_typé : Syntaxe.prog -> bool

Interface du module exécution


exception: Division_par_zero
val: exec: Syntaxe.prog -> string

Fonctions privées de ces modules :


– module typage : typ_expr : typenv -> Syntaxe.expr -> Syntaxe.typ
– module exécution : eval_expr : valenv -> Syntaxe.expr -> valeur

– typenv : Type des environnements de types

10
– valenv : Type des environnements de valeurs
Le type valeur doit être un type privé du module exécution (type somme regroupant
int, real, etc.)
Par contre les deux types d’environnements peuvent avantageusement être généra-
lisés : un type polymorphe d’environnement, défini dans un module à part.

Graphe de dépendance rectifié

programme principal

analyse syntaxique typage exécution

syntaxe abstraite environnements

Interface du module Environnements


(* type polymorphes des environnements *)
type ’a env

(plus des spécifications des exceptions et fonctions).


Est-ce que le module Environnements doit dépendre du module Syntaxe (qui défini
le type typ) quand on veut construire des environnements de typage ?
On pourrait continuer le découpage, et
– couper l’analyse syntaxique en analyse lexicale et analyse syntaxique propre
(mais là le découpage dépend plutôt des outils utilisés)
– couper le module Syntaxe en deux : Expressions et Instructions
– pareil couper les modules de typage et d’évaluation en deux (expressions et ins-
tructions)
mais c’est un peu excessif pour ce problème.

Remarques
Cas caricaturales à éviter :
– Un seul module pour le programme entier
– Un module par définition de type ou fonction

11
– Découpage arbitraire : un module pour tous les types, un autre pour toutes les
fonctions, etc.
Bon découpage :
– Correspond à la logique du programme
– Modules d’une taille raisonnable
– Définition auxiliaires cachées dans les modules (l’organisation en modules sim-
plifie la structure du programme)
– Réutilisation du code au lieu de duplication

Quelques mots sur la documentation


– Documenter la structure globale du programme (graphe de dépendance)
– Interfaces des modules : Documenter l’utilisation du module :
– Son rôle général
– Que représentent les types ?
– Spécifier les fonctions : Expliquer les rôles des arguments, les hypothèses sur
leurs valeurs (par ex : entier positif, liste triée, etc.), et bien sûr le résultat.
N’oubliez pas les cas d’erreur.
En général : La doc de l’interface doit contenir toutes les informations néces-
saires pour l’utilisation du module.
– Corps des modules :
– Spécifier les fonctions privées.
– Expliquer l’algorithme utilisée quand pas évident.
– Donner des invariants des fonctions (utiliser des construction du langage (as-
sertions si possible)

4 Le langage des modules en OCaml


En vérité : Module 6= Unité de compilation.
Il y a en OCaml des constructions pour définir des modules (à part des modules
définis implicitement par unité de compilation).
Cela permet par exemple de :
– définir un module avec plusieurs interfaces
– définir des modules qui sont paramétrés par des modules (par exemples des
tables de hachage)

module Nom = struct


<d\’efinitions des types, valeurs, et exceptions>
end

module IntStack =
struct
type stack = int list
let empty = []
let push l i = i::l

12
let rec somme l = match l with
[] -> 0
| h::r -> h+(somme r)
exception Empty_Stack
let top l = match l with
h::r -> h
| [] -> raise Empty_Stack
let pop l = match l with
h::r -> r
| [] -> raise Empty_Stack
end

Réponse de OCaml :
module IntStack :
sig
type stack = int list
val empty : ’a list
val push : ’a list -> ’a -> ’a list
val somme : int list -> int
exception Empty_Stack
val top : ’a list -> ’a
val pop : ’a list -> ’a list
end
Les types dans les signatures.
Par défaut : la signature (interface) d’une structure (module) contient tout ce qui est
défini par la structure.
On peut définir une nouvelle signature et puis masquer une partie de la structure
par la nouvelle signature :

module type STACK =


sig
type stack
val push : stack -> int -> stack
val empty : stack
exception Empty_Stack
val top : stack -> int
val pop: stack -> stack
val somme : stack -> int
end

Restriction d’une structure par une signature


Sur l’exemple :
module Stack = (IntStack : STACK)
Attention : deux types abstraits ayant la même implémentation sont incompatibles !

13
Quand doit-on considérer équivalents deux types ?
On a deux choix :
équivalence structurelle on considère un type t1 et un type t2 équivalents si leur
structure est identique. Sin on fait ce choix, le programme suivant est bien typé :
type rectype = {name:string, age:int} in
let rec1 = {name="Nobody", age=1000} in
type rectype’ = {name:string, age:int} in
let rec2= {name="Somebody", age=2000} in
rec1 = rec2

Mais cela demande un effort considérable au compilateur, en particulier si on


permet des types récursifs (on sait en décider l’équivalence, mais cela sort du
cadre du cours de cette année)

types génératifs on considère tous les types distincts, même s’ils ont la même struc-
ture3 .
Cela signifie que chaque nouvelle définition d’un type utilisateur doit être distin-
guèe de toutes les précédentes.

Pour obtenir cet effet, on associe à chaque définition de type une valeur unique
(cela peut être un numero de série, en litérature on parle de “time-stamp”), qui
permettra de le distinguer facilement et rapidement des autres.

On parle aussi de types génératifs, parce-que chaque déclaration de type produit


(génére) une nouvelle valeur unique.
Le choix fait dans les langages
Langage types génératifs Notes
Ocaml oui mais attention pour les modules
C/C++ oui pour structures, union, tableaux
non pour le reste
Pascal oui sauf pour SET
Ada oui
Algol 68 non le langage non génératif le plus complexe
Modula-3 génératif sur les types abstrait,
structurel sur les types concrets
Rappel : deux types abstraits ayant la même implémentation sont incompatibles
(générativité) ! Si on veut garder la compatibilité, il faut un mechanisme ad hoc.
3 Les langages qui font ça comportenten général une notion d’abréviationpour introduire des nouveaux

nomspour le même type

14
Partage de type
module Nom1 = (Nom2 : SIG with type t1 = t2 and ...)

(voir la demo)

Modules et sous-modules
Un module peut contenir des sous-modules. Le principe de la portée lexicale (angl. :
lexical scoping) s’applique.
Un module contient des définitions de
1. Types (concrets ou abstraits)
2. Valeur (incl. des fonctions)
3. Exceptions
4. Modules

(* Demo: modules avec plusieurs interfaces et partage de types *)


(***************************************************************)

(* Définition d’un module IntStack *)


module IntStack =
struct
type stack = int list
let empty = []
let push l i = i::l
let rec somme l = match l with
[] -> 0
| h::r -> h+(somme r)
exception Empty_Stack
let top l = match l with
h::r -> h
| [] -> raise Empty_Stack
let pop l = match l with
h::r -> r
| [] -> raise Empty_Stack
end

(* Le type stack du module IntStack est un type concret, on peut utiliser


les valeurs du type IntStack.stack comme des listes.
*)
let x = IntStack.empty;;
List.hd (IntStack.push x 42);;

(* Définition d’une signature STACK *)


module type STACK =
sig

15
type stack
val push : stack -> int -> stack
val empty : stack
exception Empty_Stack
val top : stack -> int
val pop: stack -> stack
val somme : stack -> int
end

(* Définition d’un nouveau module Stack qui est la restriction de IntStack


par la signature STACK *)
module Stack = (IntStack : STACK)

(* Le type stack du module Stack est un type abstrait, on ne peut pas le


confondre avec les listes.
*)
let x = Stack.empty;;
List.hd (Stack.push x 42);;
Stack.top (Stack.push x 42);;

(* Définition d’une nouvelle signature CONSTRUCT_STACK *)


module type CONSTRUCT_STACK =
sig
type stack
val empty: stack
val push: stack -> int -> stack
end

(* Définition d’un nouveau module CStack qui est la restriction de Stack


par la signature CONSTRUCT_STACK *)
module CStack = (Stack : CONSTRUCT_STACK)

(* Définiton d’une nouvelle signature DESTRUCT_STACK *)


module type DESTRUCT_STACK =
sig
type stack
exception Empty_Stack
val top : stack -> int
val pop: stack -> stack
end

(* Définition d’un nouveau module CStack qui est la restriction de Stack


par la signature DESTRUCT_STACK *)
module DStack = (Stack : DESTRUCT_STACK)

16
(* Problème: Les types stack des modules CStack et DStack sont des types
abstraits, l’identité de leur implantation n’est pas visible vers
l’extérieur.
*)
let x = CStack.empty;;
DStack.top (CStack.push x 42);;

(* Rédéfinition des deux modules Stack et RStack, mais cette fois avec
partage du type stack.
*)
module CStack = (Stack : CONSTRUCT_STACK with type stack = Stack.stack)
module DStack = (Stack : DESTRUCT_STACK with type stack = Stack.stack)

(* Maintenant les deux types sont égaux mais toujours abstraits *)


let x = CStack.empty;;
DStack.top (CStack.push x 42);;
List.hd (CStack.push x 42);;

(**************************************************************************)
(* Alternative: Utiliser un module local *)
(**************************************************************************)

(* Définition d’un module InstStack qui contient la définition du type


stack, puis deux modules locales qui peuvent « voir » la définition
du type stack (=> principe de la Portée Lexicale).
*)

module IntStack =
struct
type stack = int list
module CStack =
struct
let empty = []
let push l i = i::l
end
module DStack =
struct
exception Empty_Stack
let top l = match l with
h::r -> h
| [] -> raise Empty_Stack
let pop l = match l with
h::r -> r
| [] -> raise Empty_Stack
end
end

17
(* Définition d’une signature qui fait le type stack abstrait. *)
module type STACK =
sig
type stack
module CStack :
sig
val empty : stack
val push : stack -> int -> stack
end
module DStack :
sig
exception Empty_Stack
val top : stack -> int
val pop : stack -> stack
end
end

(* Restriction du module IntStack par la signature STACK *)


module Stack = (IntStack : STACK)

(* Maintenant les deux modules Stack.CStack et Stack.DStack partagent


le type abstrait stack.
*)
let x = Stack.CStack.empty;;
Stack.DStack.top (Stack.CStack.push x 42);;
List.hd (Stack.CStack.push x 42);;

(* Ou avec ouverture de l’espace de nom du module Stack *)


open Stack
let x = CStack.empty;;
DStack.top (CStack.push x 42);;
List.hd (CStack.push x 42);;

5 Modules paramétrés
5.1 Des types polymorphes au modules paramétrés
Types polymorphes
Structure des listes polymorphes ’a list : Les opérations sur les listes (hd, tl)
sont complètement indépendantes de la structure paramètre et de ses opérations.

18
Parfois les types polymorphes ne sont pas satisfaisantes
Exemple : On veut définir un type ’a set d’ensembles finis des éléments d’un
type paramètre ’a, mais on veut implanter les ensembles par des arbres équilibrés.
⇒ La réalisation des opérations sur le type ’a set utilise l’opération de compa-
raison sur le type ’a, c’est-à-dire une opération
compare : ’a -> ’a -> int
où (compare x y) renvoie
– une valeur négative quand x est plus petit que y
– 0 quand x est égal à y
– une valeur positive quand x est plus grand que y

Solution avec un type polymorphe


On définit un type polymorphe ’a set, puis on passe la fonction de comparaison
comme argument supplémentaire aux opérations sur les ensembles.
(* type polymorphe des ensembles finis sur le type ’a *)
type ’a set
val insert: ’a set -> ’a -> (’a -> ’a -> int) -> ’a set
val remove: ’a set -> ’a -> (’a -> ’a -> int) -> ’a set
...
Trop lourd, on voudrait dire une fois pour toutes quelle est l’opération de comparaison
à utiliser pour un certain type paramètre.

Solution avec un module paramétré


module Nom1 (Nom2: signature) = structure

On trouve aussi (par exemple dans la doc de OCaml :


module Nom1 = functor (Nom2: signature) -> structure
Dans la définition de structure on a accès aux définitions de la structure paramètre
(selon la signature signature.
(voir la demo)

(* Demo: Modules paramétres. *)


(***************************************************************)

(* Définition d’une signature pour les modules qui définent un type


ordonné.
*)

module type OrderedType =


sig
type t

19
val compare: t -> t -> int
end
;;

(* Définition d’une structure de paires d’entiers, avec ordre


lexicographique
*)
module OrderedIntPair =
struct
type t = int * int
let compare (x1,x2) (y1,y2) =
let d1 = x1 - y1
in if d1 = 0 then x2 - y2 else d1
end
;;

(* Définition d’un module paramétré pour les ensembles *)


module Set ( Element : OrderedType ) =
struct
type ele = Element.t
type set =
Empty
| Node of set * ele * set
let emptyset = Empty
let rec insert x = function
Empty -> Node(Empty,x,Empty)
| Node(l,y,r) -> let c = Element.compare x y in
if c < 0 then Node( (insert x l), y, r)
else if c = 0 then Node( l, y, r)
else Node( l, y, (insert x r))
let rec member x = function
Empty -> false
| Node(l,y,r) -> let c = Element.compare x y in
if c < 0 then member x l
else c = 0 || member x r
end
;;

(* Application du functor Set à la structure OrdererIntPair *)


module IntPairSet = Set ( OrderedIntPair );;

(* Tester le module IntPairSet *)


open IntPairSet;;

let s = insert (1,0) (insert (2,4) emptyset);;

20
member (1,0) s;;
member (17,42) s;;

(* Définition d’une signature qui laisse le type set abstrait *)


module type ABSET =
sig
type ele
type set
val emptyset : set
val insert : ele -> set -> set
val member : ele -> set -> bool
end

(* Définition d’un functor qui, à partir d’une structure ordonnée, construit


la structure d’ensemble avec un type abstrait set.
*)
module AbSet ( Element : OrderedType ) =
( Set ( Element ) : ABSET with type ele = Element.t )

(* Application à la structure des paires d’entiers *)


module IntPairSet = AbSet ( OrderedIntPair );;

(* Tester le nouveau module IntPairSet *)


open IntPairSet;;

let s = insert (1,0) (insert (2,4) emptyset);;


member (1,0) s;;
member (17,42) s;;

6 Encapsulation et état
L’encapsulation permet de cacher une variable dans un module, et de n’y permettre
accès que par certains fonctions. Exemple : Un compteur.

Interface
(* Module implementing a single counter. The initial value of the counter
is 0*)

(* (inrement ()) increases the value of the counter by 1 *)


val increment: unit -> unit
(* (show ()) returns the current value of the counter *)
val show: unit -> int

21
Corps
let c = ref 0
let increment () = c := !c+1
let show () = !c

Exercice: On souhaite définir un module qui permet de créer un nombre arbitraire


de compteurs. L’interface est
(* Module defining a type of counters. Instances of counter can be created
dynamically. *)

type counter = {
increment: unit -> unit;
show: unit -> int
}
(* create a new counter *)
val create: unit -> counter

Voici un exemple de l’utilisation de ce module


open Multicounter;;

(* this directive is only needed in the interactive toplevel *)


#load "multicounter.cmo";;

let a = create ();;


a.show ();;
a.increment ();;
let b = create ();;
a.increment ();;
a.show ();;
b.increment ();;
b.show ();;

Réaliser le corps de ce module.

Plan du chapitre Objets


1. Introduction - l’approche orientée objet
2. Un très bref historique des langages orientés objet
3. Les objets en OCaml
4. Héritage et classes virtuelles
5. Typage des classes en OCaml
6. Classes paramétriques
7. Objets et sous-typage

22
7 Introduction - l’approche orienté objet
7.1 Structuration par données . . .
. . . au lieu de structuration par fonctions (découpage vertical au lieu de découpage
horizontal). Voir par exemple le TD sur le découpage de l’interpréteur en modules.
Un objet est une entité autonome qui « sait » répondre à une requête.
Au lieu d’appliquer des fonctions à des données, on envoie maintenant à des objets
des messages demandant à l’objet l’exécution d’une méthode.

Avantages
– une modélisation par objets correspond souvent mieux au monde réel. Par exemple :
dans une interface graphique, on peut modéliser les fenêtres différentes par des
objets, et envoyer aux fenêtres des messages demandant d’afficher qqc ou de
changer leur configuration.
– dans un projet à longue durée et évolutif, la structuration en objets est souvent
plus stable que les spécifications des fonctions. Donc, il semble pertinent de
prendre la structuration en donnée comme base, au lieu de la structuration en
fonctionnalités qui risque de changer plus rapidement.

7.2 Des objets différents comprennent le même message


. . . mais y réagissent par l’exécution de leur implantation privée de la méthode
demandée.
Exemple : Des objets différents qui implantent tous une méthode afficher. L’im-
plantation de cette méthode, et le format d’affichage, dépendent de la nature de l’objet
qui exécute cette méthode.
La réalisation d’une fonctionnalité est localisée dans l’objet qui l’exécute et pas
centralisée dans une définition de fonction.
Conséquence sur la structure du code :
Avant on avait une structure comme
type t = Constr1 of t1 | Constr2 of t2
let affiche = function
Constr1(x) -> ......
| Constr2(x) -> ......
Avec une approche orientée objet la structure est comme suit :
classe Constr1(t1)
m\’ethode affiche -> ....
end

classe Constr(t2)
m\’ethode affiche -> ...
end

23
Puis, on peut créer des objets des classes définies et leur envoyer le message « affiche-
toi »
x := new Constr1 (argument1)
x.affiche
x := new Constr2 (argument2)
x.affiche
qui déclenche dans les deux cas l’exécution d’une méthode différente. C’est au moment
d’exécution que la méthode effectivement exécutée est déterminée (principe de la «
liaison retardée » (angl. : late binding)
(Questions laissé ouvertes pour l’instant : quel est le type de la variable x ? et ou
est le type t du programme du départ ?)
Dans la première approche : Quand on veut ajouter un nouveau constructeur Constr3
of t3 dans la définition du type t il faut changer le code de toutes les fonctions qui
font un filtrage sur un argument du type t. Ces fonctions peuvent être localisés dans
des modules différents qu’il faut donc tous ré-compiler.
Par contre dans l’approche orientée objet, on définit simplement une nouvelle classe
classe Constr3(type3)
m\’ethode affiche -> ...
end
et il n’est pas nécessaire de toucher le code des anciennes classe, ni de les ré-compiler.
Donc, l’extension de la définition des types et plus facile.
Par contre, le problème revient partiellement quand on essaye d’ajouter une nou-
velle fonction :
– dans la première approche : facile
– dans l’approche orientée objet : définir une nouvelle méthode dans toutes les
classes (il y a dans certains cas une meilleure solution à l’aide de l’héritage, voir
au-dessous)

7.3 L’héritage
On peut écrire des classes et puis les spécialiser par des sous-classes.
Les sous-classes héritent les méthodes de leur super-classe, peuvent définir des
nouvelles méthodes et même ré-définir des méthodes héritées.
classe T
m\’ethode f -> ...
end
classe Constr1 (type1)
sous-classe de T
m\’ethode afficher -> ...
end
classe Constr2 (type 2)
sous-classe de T
m\’ethode afficher -> ...
end

24
Donc solution partielle au problème d’ajout des nouvelles fonctions : quand la fonc-
tion n’est pas spécifique au classes Constr1, Constr2, alors on peut mettre la défi-
nition de la nouvelle fonction dans la classe T. Par contre, il n’y a pas de bonne solution
quand l’implantation de la méthode dépend de la sous-classe.
Réponses aux questions laissé ouvertes avant ?
– le type t est devenu la super-classe T
– le type de la variable x (qui prend comme valeurs des objets des classes Constr1
et aussi Constr2 peut être T (dépend du langage de programmation, en parti-
culier attention à la relation entre héritage et sous-typage !).

7.4 Les objets peuvent avoir un « état » . . .


. . . réalisé par des variables d’instance : variables qui sont locales à l’objet, et dont
l’accès est restreint (par exemple : seulement visibles pour les méthodes de la classe).
L’état est donc encapsulé par l’objet.
(voir l’exercice à la fin du chapitre Programmation modulaire.)
Ce n’est pas un trait essentiel des objets, on peut aussi avoir des objets purement
fonctionnels.

7.5 Récapitulatif des notions principales


Classe une classe est un ensemble agrégé de champs de données (appelés des variables
d’instance) et de traitements (appelés méthodes).
Objet un objet est un élément (ou instance) d’une classe. Un objet possède les com-
portements de la classe à laquelle il appartient. L’objet est le composant effectif
des programmes (c’est lui qui calcule) alors que la classe est plutôt une définition
ou une spécification pour l’ensemble des instances à venir.
Méthode une méthode est une action qu’un objet est à même d’effectuer.
Message un envoi de message à un objet est la demande faite à ce dernier d’exécuter
une de ses méthodes. On pourra également dire que l’on invoque une méthode.

8 Un très bref historique des langages à objets


8.1 SIMULA
– 1967 (Ole-Johan Dahl et Kristen Nygaard)
– basé sur A LGOL (un grand classique de la programmation impérative et des lan-
gages structurés)
– Domaine d’application : simulations des systèmes complexes
– tout sur l’histoire de S IMULA : http://java.sun.com/people/jag/
SimulaHistory.html

25
8.2 Smalltalk
– 1976 (Adele Goldberg de chez XEROX PARC)
– langage non-typé, un peu dans la tradition de L ISP (un grand classique de la
programmation fonctionnelle)
– tout est un objet, même les nombres et les classes
– environnement graphique très innovateur (fenêtres, icônes, pointeur, qui sont
modélisés comme des objets S MALLTALK)

8.3 À partir des années 80


– E IFFEL (Bertrand Meyer)
– C++ (Bjarne Stroustrup)
– Des extensions objets pour tous les langages : Ada 95, Clos (pour LISP), Turbo
PASCAL, même pour C OBOL
– et bien sûr JAVA (de chez S UN) mais ce langage a certainement aussi profité du
succès de l’internet. Autre aspect important : mobilité de code (« applets »)

9 Les objets on Objective CAML


9.1 Définition d’une classe
class nom p1 ... pn = object
...
variables d’instance
...
m\’ethodes
end

où p1 , . . . , pn sont les paramètres que prendra le constructeur de cette classe. Une


classe peut n’avoir aucun paramètre.
Les « variables d’instance » se déclarent dans une des deux formes
val nom = expr
val mutable nom = expr
Toutes les variables d’instances sont visibles pour toutes les méthodes de la classe,
mais elles ne sont pas visibles vers l’extérieur. L’accès de l’extérieur aux variables
d’instances ne se fait que par les méthodes de la classe.
Dans le deuxième cas la variable est aussi modifiable par toutes les méthodes de
cette classe, à l’aide de la construction (comme pour les champs mutables des enregis-
trements).
nom <- expr
Une méthode est déclarée par la construction
method nom p1 ...pn = expr

26
où p1 , . . . , pn sont les paramètres que prendra une invocation de cette méthode. Une
méthode peut n’avoir aucun paramètre (à distinguer du cas d’un paramètre () ).

9.2 Création d’un objet


Un objet d’une classe nom avec paramètres de types t1 , . . . , tn est créé par
new nom exp1 ... expn
où les expi sont des expresions de type ti .

Exemple (voir la démo objets1.ml)


(* Une classe très simple : les compteurs. On peut la simuler parfaitement
dans CAML sans les objets.
*)

class counter (initial) = object


val mutable c = initial
method increment () = c <- c+1
method show () = c
end

(* Maintenant on peut créer deux compteurs indépendants *)


let x = new counter 0;;
x#increment ();;
x#increment ();;
x#show ();;
let y = new counter 42;;
y#increment ();;
y#show ();;
x#show ();;

(* les variables d’instaces ne sont pas visibles *)


x#c;; (* erreur *)

(* Simulation en CAML sans objets. La variable d’instance x de la classe est


ici simulée par un identificateur local qui n’est visible que dans les
clôtures des fonctions increment et show.
*)

type counter = {
increment: unit -> unit;
show: unit -> int
}

let create_counter (initial) =


let c = ref initial
in

27
{
increment = (function () -> c := !c+1);
show = (function () -> !c)
}

let x = create_counter 0;;


x.increment ();;
x.increment ();;
x.show ();;
let y = create_counter 42;;
y.increment ();;
y.show ();;
x.show ();;

(* l’identificateur c n’est pas un champ des enregistrements du type counter *)


x.c;; (* erreur *)

(voir le schema comment traduire les classes simples en OCaml sans objets)

9.3 Type d’objet 6= classe


Le type d’un objet est constitué des types des méthodes qu’il peut exécuter. Consé-
quences :
– Les types des variables d’instances ne figurent pas dans le type d’un objet.
– Le nom de la classe est simplement une abbréviation pour l’ensemble des types
de ses méthodes.
– Deux objets de classe différente peuvent avoir le même type.
Relation d’égalité entre objets : Un objet est seulement égal à lui même (identité phy-
sique). Deux objets créés indépendamment sont toujours différents, même s’ils ont les
mêmes implantations des méthodes et les mêmes variables d’instances avec les mêmes
valeurs. (voir la démo objet-types.ml)
(* Deux classes du même type *)

class counter (initial) = object


val mutable c = initial
method increment () = c <- c+1
method show () = string_of_int c
end

class counter2 (initial1, initial2) = object


val mutable c1 = initial1
val mutable c2 = initial2
method increment () = c1 <- c1 + 1 ; c2 <- c2 * c2
method show () = "[" ^ (string_of_int c1) ^ " , " ^ (string_of_int c2) ^ "]"
end

let x = new counter 0;;


x#increment ();;

28
x#show ();;
let y = new counter2 (1,2);;
y#increment ();;
y#show ();;
x=y;; (* x et y sont du même type ! *)

(* les noms des classes sont simplement des abbréviations pour le type *)
(x:counter) = (y:counter2);;

(* Définition d’une classe de type différent *)

class counter3 (initial) = object


val mutable c = initial
method incrementer () = c <- c+1
method montrer () = string_of_int c
end

let z = new counter3 0;;

x = z ;; (* les types de x et z sont différents *)

(* Quand est-ce que deux objets sont égaux *)

let x1 = new counter 0;;


let x2 = new counter 0;;

x1 = x2 ;;
x1 = x1;;

(* chaque objet à sa propre identité *)

(* Simulation partielle en CAML sans objets. *)

type counter_type = {
increment: unit -> unit;
show: unit -> string
}

let create_counter (initial) =


let c = ref initial
in
{
increment = (function () -> c := !c+1);
show = (function () -> string_of_int !c)
};;

let create_counter2 (initial1, initial2) =

29
let c1 = ref initial1
and c2 = ref initial2
in
{
increment = (function () -> c1 := !c1+1; c2 := !c2*(!c2));
show = (function () ->
"[" ^ (string_of_int !c1) ^ " , " ^ (string_of_int !c2) ^ "]")
};;

(* les deux fonctions de création renvoient le même type *)


create_counter;;
create_counter2;;

let x = create_counter 0;;


x.increment ();;
x.show ();;
let y = create_counter2 (1,2);;
y.increment ();;
y.show ();;

x = x;;

(* cela ne marche pas parceque OCaml ne peut pas comparer les fonctions *)
x = y;;

(* L’identité des valeurs de type « enregistrement de fonctions » est


différent de l’identité d’objets.
*)

9.4 Méthodes récursives et self


Dans la déclaration d’une classe on peut donner un nom qu’un objet peut utiliser
pour envoyer un message à lui-même :
class nom p1 ...pn = object (self)
...
end
Le nom peut être choisi librement (dans autres langages orienté objets : mot clef pour
designer l’objet lui-même self, ou this en C++ ).
Cela permet par exemple de réaliser des méthodes récursives :
(* Démonstration des méthode récursives *)

class calc(initial) = object(self)


val mutable c = initial
method increment () = c <- c+1
method show () = c
method fact (i) =

30
if i = 0
then 1
else i * self#fact(i-1)
end

let x = new calc (0) ;;


x#fact(4);;

(* Simulation en OCaml sans objets *)

type calc = {
increment: unit -> unit;
show: unit -> int;
fact: int -> int
}

let create_calc initial =


let c = ref initial
in
let rec
increment = (function () -> c := !c+1)
and
show = (function () -> !c)
and
fact(i) =
if i=0
then 1
else i * fact (i-1)
in
{
increment = increment;
show = show;
fact = fact
}

let x = create_calc 0;;


x.fact (4);;

Dans cet exemple, nous avons vu comment généraliser le schema de traduction aux
objets qui peuvent envoyer des messages à eux-mêmes.

Il s’agit cependant d’une première solution naïve, parce-que elle ne resistera pas à
l’héritage, qui va exhiber la nature de recursion ouverte des messages faisant envoyés
à self.

31
9.5 Agrégation d’objets
Première relation importante entre classes : agrégation (l’autre rélation importante
est l’héritage, voir plus tard) : Une classe peut définir des variables d’instances d’un
type objet (listes d’objets etc.).
Notion graphique de UML (Unified Modelling Language) pour les classes et la
rélation d’agrégation.

(* définition d’une première classe pour les points *)

class point (x_init,y_init) =


object
val mutable x = x_init
val mutable y = y_init
method get_x = x
method get_y = y
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
method distance () = sqrt (float(x*x + y*y))
end ;;

let x = new point(1,1);;


x#distance ();;
x#rmoveto(2,3);;
x#distance ();;

(* définition d’une classe d’images qui contient un tableau de points *)


class picture n =
(* l’argument n est le nombre maximal de points *)
object
val tab = Array.create n (new point(0,0))
val mutable ind = 0
(* la première case non occuppée *)
method add p =
try tab.(ind)<-p ; ind <- ind + 1
with Invalid_argument("Array.set")
-> failwith ("picture.add:ind =" ^ (string_of_int ind))
method remove () = if (ind > 0) then ind <-ind-1
method to_string () =

32
let s = ref "["
in for i=0 to ind-1 do s:= !s ^ " " ^ tab.(i)#to_string() done ;
(!s) ^ "]"
end ;;

let p = new picture 5;;


p#to_string ();;
p#add x;;
p#add (new point (17,42));;
p#to_string ();;

10 Héritage et classes virtuelles


10.1 Syntaxe de l’héritage
Dans le corps d’une classe on peut déclarer qu’on souhaite hériter les variables
d’instances et les méthodes d’une autre classe :

inherit nom1 p1 ...pn [ as nom2 ]

où nom1 est le nom de la classe de laquelle on veut hériter, p1 , . . . , pn sont les pa-
ramètres nécessaires pour la création d’une instance de la classe nom 1 , et nom2 est
un nom qu’on peut utiliser pour faire directement référence à des méthodes héritées,
ce qui correspond au mot clef super dans d’autres langages orientés objet (voir plus
tard).
Puis, une classe peut ré-définir des variables d’instances et des méthodes héritées.
1. la ré-définition d’une méthode doit conserver le type de la méthode
2. l’ancienne définition d’une méthode ré-définie est toujours accessible à l’aide du
préfixe nom2#
Notation graphique de UML pour la relation d’héritage.

(* définition d’une première classe pour les points *)

class point (x_init,y_init) =


object
val mutable x = x_init
val mutable y = y_init

33
method get_x = x
method get_y = y
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
method distance () = sqrt (float(x*x + y*y))
end ;;

let x = new point(1,1);;


x#to_string ();;

(* définition d’une deuxième classe de points colorés *)


class colored_point (x,y) c =
object
inherit point (x,y)
val mutable c = c
method get_color = c
method set_color nc = c <- nc
method to_string () = "( " ^ (string_of_int x) ^
", " ^ (string_of_int y) ^ ")" ^
" [" ^ c ^ "] "
end ;;

let y = new colored_point(3,4) "rouge";;


y#to_string ();; (* la réalisation de cette méthode a changée *)
y#distance ();; (* méthode héritée *)

(* mieux : utiliser la méthode to_string de la classe point *)


(* pour la rédéfinition de la méthode to_string *)
class colored_point (x,y) c =
object
inherit point (x,y) as super
val mutable c = c
method get_color = c
method set_color nc = c <- nc
method to_string () = super#to_string() ^ " [" ^ c ^ "] "
end ;;

let y = new colored_point(3,4) "rouge";;


y#to_string ();; (* la réalisation de cette méthode a changée *)
y#distance ();; (* méthode héritée *)

Première traduction naïve de l’héritage.

10.2 Liaison retardée


Envoi de message 6= appel d’une fonction !

34
(* modification de l’exemple précédent *)

class point (x_init,y_init) =


object(self)
val mutable x = x_init
val mutable y = y_init
method get_x = x
method get_y = y
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
method distance () = sqrt (float(x*x + y*y))
method print () = (* nouvelle méthode *)
print_string ("==> " ^ (self#to_string () ) ^ " <==\n")
end ;;

let x = new point(1,1);;


x#print ();;

(* définition d’une deuxième classe de points colorés *)


class colored_point (x,y) c =
object
inherit point (x,y) as super
val mutable c = c
method get_color = c
method set_color nc = c <- nc
method to_string () = super#to_string () ^ " [" ^ c ^ "] "
end ;;

let y = new colored_point(3,4) "rouge";;


y#print ();;

Exercice: Expliquer comment traduire des définitions de classes en Caml sans ob-
jets et avec une discipline de typage relâchée, tel que le principe de la liaison retardé
est conservé.
Exercice: Quelles sont les réponses aux deux invocations de méthodes dans le pro-
gramme suivant (fichier super.ml) :
class c1 = object
method m1 = "a"
end

class c2 = object (self)


inherit c1 as super
method m1 = "b"
method m2 = super#m1
method m3 = self#m1
end

35
class c3 = object
inherit c2 as super
method m1 = "c"
end

class c3’ = object (self)


inherit c1
method m4=self#m1
inherit c2
end

let x = new c3;;


x#m2;; (* quel est le résultat ? *)
x#m3;; (* quel est le résultat ? *)

10.3 Héritage multiple


Il peut être utile de hériter de plusieurs classes à la fois. Par exemple, en aurait pu
définir la classe des point colorés par héritage des deux classes point et couleur.

(* Démonstration du héritage multiple *)

class point (x_init,y_init) = object


val mutable x = x_init
val mutable y = y_init
method get_x = x
method get_y = y
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
end

class colored color_init = object


val mutable c = color_init
method set_color color_new = c <- color_new
method to_string () = "[ " ^ c ^ " ]"
method inverse () = c <- match c with
"black" -> "white"
| "white" -> "black"
| "red" -> "green"
| "green" -> "red"
| any -> any
end

class colored_point (x_init,y_init) color_init = object


inherit point (x_init,y_init) as position
inherit colored color_init as color

36
method to_string () = (position#to_string ()) ^ (color#to_string ())
end

let x = new colored_point (0,0) "white";;


x#moveto (17,42);;
x#inverse ();;
x#to_string ();;

Problème : quoi faire quand une méthode ou variable est définie dans plusieurs
classes ancêtres ?
Classe A : défini une variable x
Classe B : défini une variable x
Classe C : hérite à la fois de A et de B
La variable x de A est différente de la variable x de B, il faut trouver une solution
pour résoudre le conflit, par exemple :
– c’est la dernière définition qui cache les définitions antérieures
– construction pour renommer les variables et méthodes au moment de l’héritage
La solution est moins évidente quand les deux variables x viennent à l’origine de
la même définition, par exemple :
Classe A : défini une variable x
Classe B : hérite de A, contient donc une variable x
Classe C : hérite de B, contient donc une variable x
Classe D : hérite à la fois de B et de C. On peut argumenter que D doit contenir les
deux copies de x ou une seule.

Exemple 1
Classe A : classe des engins à moteur, qui contient comme champs les caractéris-
tiques du moteur (puissance, consommation, etc.)
Classe B : classe des automobiles, obtenue par héritage de A
Classe C : classe des grues, obtenue par héritage de A
Classe D : classe des grues automotrices, obtenue par double héritage de B et de C.
On voudrait que le moteur « automobile » soit distinct du moteur « grue »

Exemple 2
Classe A : classe des objets mobiles, avec une variable vitesse
Classe B : classe des bateaux, obtenue par héritage de A
Classe C : classe de objets propulsés par le vent, obtenue par héritage de A
Classe D : classe des bateaux à voile, obtenue par double héritage de B et de C. On
voudrait qu’il y ait une seule variable vitesse.

La bonne solution ?
La solution la plus propre est de retenir une seule instance de la variable obtenue
par plusieurs « chemins » de héritage.

37
Dans l’exemple 1 : c’est plutôt un abus de l’héritage. Une meilleure modélisation
est de hériter d’une seule classe (par ex. automobile), et de définir dans la classe des
grues automotrices une variable d’instance qui contient le moteur de la grue.
Mais : problème de complexité de déterminer si deux définitions de la même va-
riable ont le même origine.
Exercice: Proposer un expérience qui permet de déterminer quelle est la solution
choisie en OCaml.

10.4 Variables de classes


Nous avons vu les variables d’instances des classes : chaque objet qui est instance
d’une telle classe possède une instance de cette variable.
Il y a normalement dans les langages orientés objet aussi un mécanisme pour définir
des variables de classe. Ce sont des variables qui existent une fois par classe et qui sont
donc partagées entre tous les objets d’une telle classe.
En OCaml : il n’existe pas de construction syntaxique pour les variables de classe
mais on peut les simuler par des variables qui sont globales à l’objet.

let g = ref 0

class c = object
method incr () = g := !g +1
method show () = !g
end

let x = new c;;


let y = new c;;
x#show ();;
y#incr ();;
x#show ();;

Cette solution a le désavantage que la variable est complètement globale et donc


visible pour tout le monde.
Pour avoir une vraie variable de classe qui n’est visible que pour les objets de cette
classe il faut utiliser une généralisation de la construction des classes :
class <nom> = exp end
où exp est une expression dont le type est une fonction qui renvoie un objet. En parti-
culier,
class c =
fun x1 x2 ... xn ->
object
...
end
peut être abrégé comme

38
class c x1 x2 ... xn = object
...
end
Mais la construction ci-dessus est plus générale

class c =
let instances = ref 0
in
fun n ->
object
val mutable x = n
initializer instances := !instances + 1
method incr = x <- x+1
method show = x
method number_of_brothers () = !instances -1
end

let x = new c 0;;


x#number_of_brothers ();;
let y = new c 53;;
let z = new c 82;;
x#number_of_brothers ();;

Exercice: Comment étendre la traduction des objets au mécanisme de définition de


classes plus générale vue au-dessus ?

10.5 Classes et méthodes virtuelles


Les classes virtuelles (aussi appelées des classes abstraites) ne sont pas prévues
pour être instanciées, mais ne servent que pour définir une étape dans une hiérarchie de
héritage.
En OCaml on peut définir des classes virtuelles à l’aide de la construction
class virtual <nom> = ....
On ne peut pas effectuer un new pour une classe virtuelle.
Une méthode virtuelle n’est que déclarée avec son type. Une classe contenant une
méthode virtuelle doit être définie comme une classe virtuelle.
method virtual <nom> : <type>

(* Démonstration des classes virtuelles *)

class virtual printable = object(self)


method print () = print_string (self#to_string ()); print_newline ()
method virtual to_string : unit -> string
end

let x = new printable;; (* erreur *)

39
class point (x_init,y_init) =
object
inherit printable
val mutable x = x_init
val mutable y = y_init
method get_x = x
method get_y = y
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
end

class counter (init) =


object
inherit printable
val mutable c = init
method incr = c <- c+1
method to_string () = string_of_int c
end

let p = new point (0,1);;


p#print ();;
let q = new counter (42);;
q#print ();;

L’intérêt des classes virtuelles :


– on peut déclarer une classe avec une méthode dont la définition est spécifique à
des classes dérivées
– on peut « coercer » des objets des classes dérivées vers la classe ancêtre (voir
plus tard).
– on peut spécifier le type d’une méthode (voir l’exercice suivant)
Exercice: On désire définir une classe virtuelle intlist des listes d’entier comme
class virtual intliste = object
method virtual hd : unit -> int
method virtual tl : unit -> intliste
method virtual sum : unit -> int
method virtual append : intliste -> intliste
method virtual to_string : unit -> string
end
et puis la spécialiser par deux classes concrètes cons et nil. Après avoir défini ces deux
classes, on pourra par exemple faire
let x = new cons 1 (new cons 2 (new cons 3 (new nil)));;
x#sum ();; (* 6 *)
x#to_string ();; (* "1, 2, 3, NIL" *)
let y = x#append x;;
y#to_string ();; (* "1, 2, 3, 1, 2, 3, NIL" *)

40
Donner les définitions des deux classes cons et nil. Faites attention au fait que (jusqu‘ici)
les types des méthodes ne peuvent pas être polymorphes.

10.6 Initialisateurs
On peut demander l’évaluation d’une expression lors de la création d’un objet :
initialiser expr.

class c1 (x) = object


val sq = x
initializer print_string ("Création de c1 de " ^ (string_of_int sq) ^"\n")
end

class c2 (x) = object


val id = x
initializer print_string ("Création de c2 avant de " ^ (string_of_int id) ^"\n")
inherit c1(x*x)
initializer print_string ("Création de c2 apres de " ^ (string_of_int id) ^"\n")
end

let x = new c2 42;;

10.7 Restrictions de visibilité


Il y a dans les langages orienté objets un nombre de constructions qui permettent
de restreindre la visibilité des méthodes.
Par exemple en C++, une méthode peut être
publique Visible partout (défaut)
privée Seulement visible par les méthodes de la classe même, et pas par les classe
dérivées.
protégée Visible pour les méthodes de la classe et ses classes dérivées.
De plus, on peut contourner ces mécanismes de protection par des déclarations des
classes amies : Si une classe C1 déclare une classe C2 son amie alors toutes les mé-
thodes de C1 sont visibles par les méthodes de la classe C2.
En OCaml : Il y a des méthodes privées qui correspondent aux méthodes protégées
de C++. Voir la démo private.ml :
class point (x_init,y_init) =
object
val mutable x = x_init
val mutable y = y_init
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
end

41
class point_mem (x0,y0) =
object(self)
inherit point (x0,y0) as super
val mutable old_x = x0
val mutable old_y = y0
method private mem_pos () = old_x <- x ; old_y <- y
method moveto (x1, y1) = self#mem_pos () ; super#moveto (x1, y1)
method rmoveto (x1, y1) = self#mem_pos () ; super#rmoveto (x1, y1)
method undo () = x <- old_x; y <- old_y
end

let p = new point_mem (0,0);;


p#moveto(42,42);;
p#rmoveto(17,17);;
p#to_string ();;
p#undo ();;
p#to_string ();;

(* la méthode mem_pos est privée, elle ne peut pas être invoquée de


l’extérieur
*)

p#mem_pos ();; (* erreur *)

p = new point (42,42);; (* erreur de typage *)

(* les methodes privées sont heritées par les classes dérivées *)

class colored_point_mem (x0,y0) color =


object(self)
inherit point_mem (x0,y0) as super
val c = color
method to_string () = super#to_string () ^ " de couleur " ^ c
end

let p = new colored_point_mem (0,0) "pink";;


p#moveto(42,42);;
p#rmoveto(17,17);;
p#to_string ();;
p#undo ();;
p#to_string ();;

Il n’y a en OCaml pas de construction syntaxique pour les classes amies.

42
11 Typage des classes en OCaml
En OCaml, les définitions de types ne peuvent pas conténir des variables de types
libres :
type t = ’a list
déclenche une erreur « Unbound type parameter ’a » .
Puisque la définition d’une classe entraîne une définition de type, les types des
méthodes et variables ne doivent pas contenir des variables de types libres. Il y a deux
solutions possibles quand le type inféré d’une classe n’est pas clos :
1. Ajouter des contraintes de types
2. Définir une classe polymorphe

11.1 Contraintes de types


Avec des contraintes de type de la forme (expr : type) on peut contraindre
les types des arguments des classes et des méthodes, et aussi les types des résultats des
méthodes.

11.2 Types ouverts


En Caml « classique » : tout nom de champ est spécifique à un type d’enregistre-
ment, on peut donc dériver d’un terme comme x.nom l’information que le type de
x est le type unique qui contient le champ nom (ou déclencher une erreur de typage
quand il n’y a pas de tel type).
Avec les objets : Plusieurs classes peuvent conténir la même méthode, on ne peut
donc pas dériver d’un appel de méthode o#meth la classe de l’objet o.
⇒ On a maintenat besoin d’un système de types plus souple qui peut exprimer
qu’un objet a au moins telle et telle méthode, avec tel type. En OCaml, ces types sont
appelés des types ouverts.
Un type
< m1: t1 -> s1; m2: t2 -> s2; .. >
dénote tous les objets qui ont au moins une méthode m1 du type t 1 → s1 , et une
méthode m2 du type t2 → s2 . Notez que les .. sont une partie syntaxique de cette
expression de type. (voir la démo open1).
(* Démonstration des types ouverts *)

(* la seule information sur l’argument de la fonction f qu’on peut


inférer est que x possède une méthode get_x sans argument *)
let f o = o#get_x ;;

(* les deux classes des points et des point colorés, comme avant *)

class point (x_init,y_init) =

43
object
val mutable x = x_init
val mutable y = y_init
method get_x = x
method get_y = y
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
end

class colored_point (x,y) c =


object
inherit point (x,y) as super
val mutable c = c
method get_color = c
method set_color nc = c <- nc
method to_string () = super#to_string () ^ " [" ^ c ^ "] "
end

let x = new point (0,1);;


let y = new colored_point (2,3) "vert";;
f x;;
f y;;
Les arguments d’une classe peuvent être d’un type conténant des variables libres :
La fonction de création d’instances de cette classe est simplemenmt une fonction poly-
morphe (voir la démo open2).
(* Exemple d’une classe où les paramêtres sont d’un type libre *)

class couple (a,b) =


object
val p0 = a
val p1 = b
method to_string() = p0#to_string() ^ ", " ^ p1#to_string()
method copy () = new couple (p0,p1)
end

class newint i = object


val c = i
method to_string () = string_of_int i
end

class newstring s = object


val c = s
method to_string () = "[" ^ s ^ "]"
end

let x = new couple (new newint 1, new newstring "bonjour");;


let y = x#copy ();;

44
y#to_string ();;
Par contre, les types des méthodes doivent être fermés. Ajouter des contraintes de
types si nécessaire (voir la démo open3).
(* une classe virtuelle pour les objets avec méthode « print » *)

class virtual printable = object(self)


method print () = print_string (self#to_string ()); print_newline ()
method virtual to_string : unit -> string
end

(* la classe « point » comme d’habitude *)


class point (x_init,y_init) =
object
inherit printable
val mutable x = x_init
val mutable y = y_init
method get_x = x
method get_y = y
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
method distance () = sqrt (float(x*x + y*y))
end

(* cela ne marche pas parce que le type de la méthode « m » sera ouvert *)


class t p = object
val x = p
method m () = p#to_string ()
end

(* première solution : dire que p possède au moins les méthodes de la classe


printable *)
class t (p : #printable) = object
val x = p
method m () = x#to_string ()
end

(* deuxième solution : contraindre le type du résultat de la méthode m *)


class t p = object
val x = p
method m = (x#to_string () : string)
end

let x = new point (0,1);;


let y = new t x;;
y#m;;
Attention : un type ouvert est considéré comme un type qui contient une variable
de type libre (qui dénote le « reste » du type ). Voir la démo open4 :

45
class virtual printable = object(self)
method print () = print_string (self#to_string ()); print_newline ()
method virtual to_string : unit -> string
end

(* cela déclenche une erreur puisque le type de p n’est pas connu *)


class t p = object
val x = p
method get () = x
end

(* contraindre p par un type ouvert n’est pas suffisant *)


class t (p: #printable) = object
val x = p
method get () = x
end

(* avec une contrainte de type fermé ca marche ... *)


class t (p: printable) = object
val x = p
method get () = x
end
;;

Il y a une seule exception à la règle que le type d’une méthode ne doit pas contenir une
variable de type libre : Le type d’une méthode peut contenir une variable de type qui
dénote le type de la classe elle même. (voir la démo selftype)
(* toujours la classe point *)
class point (x_init,y_init) =
object
val mutable x = x_init
val mutable y = y_init
method get_x = x
method get_y = y
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
end;;

(* classe dérivée avec une opération d’égalité *)


class point_eq (x,y) =
object (self : ’a)
inherit point (x,y)
method eq (p:’a) = (self#get_x = p#get_x) && (self#get_y = p#get_y)
end ;;

(* class dérivée avec coleur *)


class colored_point_eq (xc,yc) c =

46
object (self : ’a)
inherit point_eq (xc,yc) as super
val c = (c:string)
method get_c = c
method eq (pc : ’a) = (self#get_x = pc#get_x) && (self#get_y = pc#get_y)
&& (self#get_c = pc#get_c)
end

12 Classes paramétriques
On peut définir des classes paramétriques (voir la démo parameter)
(* erreur : variable de type libre *)
class pair x0 y0 =
object
val x = x0
val y = y0
method fst = x
method snd = y
end

(* classe paramétrique *)
class [’a,’b] pair (x0:’a) (y0:’b) =
object
val x = x0
val y = y0
method fst = x
method snd = y
end

let p = new pair 2 ’X’;;


p#fst;;
let q = new pair 3.12 true;;
q#snd;;

(* il faut indiquer les paramètres d’une classe quand on hérite d’une classe
paramétrée *)

class [’a,’b] acc_pair (x0 : ’a) (y0 : ’b) =


object
inherit [’a,’b] pair x0 y0
method get1 z = if x = z then y else raise Not_found
end

let p = new acc_pair 3 true;;


p#get1 3;;

(* Ici, la classe par_point n’est pas paramétrique *)


class pair_point (p1,p2) =

47
object
inherit [point,point] pair p1 p2
end

(* démonstration des contraintes de types *)


class virtual printable = object(self)
method print () = print_string (self#to_string ()); print_newline ()
method virtual to_string : unit -> string
end

class printable_pair (x0 ) (y0 ) =


object
inherit [printable, printable] acc_pair x0 y0
method print () = x#print(); y#print ()
end

(* on ne peut pas utiliser cette classe pour des objets d’une classe autre
que printable *)

class point (x_init,y_init) =


object
inherit printable
val mutable x = x_init
val mutable y = y_init
method get_x = x
method get_y = y
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
end

let p = new point (0,1);;


let q = new point (2,3);;

let r = new printable_pair p q;;

(* premier essai : « ouvrir » le type de printable *)

class printable_pair (x0 ) (y0 ) =


object
inherit [ #printable, #printable ] acc_pair x0 y0
method print () = x#print(); y#print ()
end

(* la bonne solution consiste en l’utilisation d’une contrainte *)


class [’a,’b] printable_pair (x0 ) (y0 ) =
object
constraint ’a = #printable
constraint ’b = #printable

48
inherit [’a,’b] acc_pair x0 y0
method print () = x#print(); y#print ()
end

let r = new printable_pair p q;;


r#print();;

13 Objets et sous-typage
13.1 Types ouverts, sous-typage et contraintes
Dans le λ-calcul « classique » : le type principal d’une expression peut être obtenu
par unification :
– pour chaque identificateur il y a une variable qui dénote son type
– on obtient d’une expression un système d’équations qui représente toutes les
contraintes de typage. Par exemple, de l’expression

x + f (x)

on obtient le système

x = Int f r = Int
f = fa → fr fa = x

dont la restriction du mgu aux variables de départ {x, f } est

x = Int
f = Int → Int

Comment intégrer les types d’objets ?


⇒ Étendre le langage de type par des types d’enregistrements :

A ::= T | A × A | A → A |< C : A, . . . , C : A >

où C est une ensemble donné de noms de champs


Comment intégrer les types d’objets ouverts ?
Les types ouverts correspondent à la notion de sous-typage !
⇒ Étendre le langage de contraintes (qui pour l’instant ne contient que la relation
d’égalité) par des inégalités : x ≤ y.
Définition d’une structure (dans le sens de la logique du premier ordre) :
– langage : constantes de T , opérateurs binaires × et →, et pour tout ensemble
{c1 , . . . , cn } de noms de champs une opération d’arité n : < c1 , . . . , cn >. Pré-
dicats binaires : = et ≤ .
– domaine : dans une première approche les types selon la grammaire au-dessus.
– interprétation des symboles de fonctions : comme dans les structures de Her-
brand
– interprétation du symbole = : égalité

49
– interprétation du symbole ≤ : t ≤ s est vrai ssi
– soit t et s sont des enregistrements, et

t = < c 1 : t 1 , . . . , c n : t n , d 1 : u1 , . . . , s m : um >
s = < c 1 : s1 , . . . , cn : sn >

avec ti ≤ si
– soit t et s sont des types produits, et

t = t1 × t2
s = s 1 × s2

avec t1 ≤ s1 et t2 ≤ s2
– soit t et s sont des types fonctionnels, et

t = t 1 → t2
s = s 1 → s2

avec t1 ≥ s1 (contra-variance !) et t2 ≤ s2
– soit t et s sont des types de base, et t = s. (Ici il y a des variantes, on pourrait
aussi imaginer des relation de sous-typage non triviales entre types de base,
comme Int ≤ Real.)
Le problème d’unification devient maintenant un problème de satisfaisabilité de
contraintes : Étant donnée une conjonction de formules positives atomiques, est-elle
satisfaisable dans la structure fixée ?
Exemple 1 : 1 + x#m(2) donne lieu au système de contraintes

x ≤ <m:t>
t = t 1 → t2
t1 = Int
t2 = Int

Toutes les types d’objets qui contiennent au moins < m : Int → Int > en sont
des solutions.
Exemple 2 : 1 + x#m(x) donne lieu au système de contraintes

x ≤ <m:t>
t = t 1 → t2
t1 = x
t2 = Int

Toutes les types t qui satisfont la relation t ≤< m : t → Int > en sont des
solutions. Mais avec notre définition de types un tel type n’existe pas !
Pourtant, OCaml accepte une telle expression (voir la démo open5) :

50
let f = function x -> 1 + x#m(x);;

class moo = object(self:’a)


method m (x:’a) = 42
end;;

f (new moo);;

⇒ Il faut changer la définition des types (et de la structure qui interprète les contraintes
de typage) et permettre aussi des types infinis !

13.2 Coercion de type


Si un objet o est d’un type t1 et t1 est un sous-type de t2 , alors on peut coercer o
vers le type t2 . Notion en OCaml :
(nom :> type)
Voir la démo coercion :
(* Démonstration de la coercion *)

class virtual printable = object(self)


method print () = print_string (self#to_string ()); print_newline ()
method virtual to_string : unit -> string
end

class point (x_init,y_init) =


object
inherit printable
val mutable x = x_init
val mutable y = y_init
method get_x = x
method get_y = y
method moveto (a,b) = x <- a ; y <- b
method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy
method to_string () =
"(" ^ (string_of_int x) ^ ", " ^ (string_of_int y) ^")"
end

class colored_point (x,y) c =


object
inherit point (x,y) as super
val mutable c = c
method get_color = c
method set_color nc = c <- nc
method to_string () = super#to_string () ^ " [" ^ c ^ "] "
end

let x = new point (1,2);;

51
let y = new colored_point (3,4) "pink";;
let z = [x; y];; (* erreur de typage *)
let z = [ (x :> printable); (y :> printable) ];; (* ca marche *)
List.iter (function o -> o#print () ) z ;; (* imprimer les objets *)

(* La même chose avec coercion vers point *)


let z = [ x; (y :> point) ];; (* ca marche *)
List.iter (function o -> o#print () ) z ;; (* imprimer les objets *)

(* c’est bien la méthode print de la classe colored_point qui est exécutée


pour y ! *)

13.3 Sous-typage 6= Héritage


On trouve parfois dans la littérature la fausse idée que l’héritage est une forme de
sous-typage. On vérité, les deux concepts sont indépendants !

Exemple d’un sous-typage sans héritage


Voir la démo soustypes1 :
class c1 = object
method m1 = "coocoo"
method m2 = "toto"
end

class c2 = object
method m1 = "hello"
end

(* il n’y a aucune relation d’héritage entre les classes c1 et c2 *)

let x1 = new c1;;


let x2 = new c2;;

(* pourtant, le type de c1 est un sous-type de du type de c2 *)


let x3 = (x1 :> c2);;
x3#m1;;

Exemple d’un héritage sans sous-typage


Voir la démo soustypes2 :
class point ( (x0:int) ,(y0:int) ) =
object(self: ’a)
val x = x0
val y = y0
method getx = x
method gety = y
method equal (p:’a) = (self#getx = p#getx) && (self#gety = p#gety)

52
end

class colored_point ( (x0:int) ,(y0:int) ) (c0:string) =


object(self: ’a)
inherit point(x0,y0) as super
val c = c0
method getc = c
method equal (p:’a) =
(self#getx = p#getx) && (self#gety = p#gety) && (self#getc = p#getc)
end

let x = new point (0,1);;


x#equal x;;
let y = new colored_point (0,1) "red";;
let z = new colored_point (0,1) "black";;
y#equal z;;

(* on ne peut pas pas comparer un point avec un point coloré *)


x#equal y;;
y#equal x;;

(* de plus, on ne peut pas coercer un colored_point vers un point *)


(y :> point);;

14 Design patterns
Quand on programme en orienté objet, dans des langages sans typage fort, il est
très important de respecter une certaine discipline de programmation, telle qu’elle peut
se trouver par exemple dans
Design Patterns (1994)
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides

Plan du chapitre Programmation concurrente


1. Introduction
2. Programmation concurrente dans le monde fonctionnel (évaluation retardée)
3. Programmation concurrente avec mémoire répartie (processus)
4. Programmation concurrente avec mémoire partagée (threads)

15 Introduction : Programmation concurrente


Un programme complet peut consister de plusieurs « filières » de calcul plus au
moins indépendantes. Ces filières sont couplées par

53
– synchronisation : attente d’une condition sur plusieurs filières
– communication : une filière envoie une information à une autre filière, ou met
une information accessible pour une autre filière.
Les mécanisme de programmation concurrente diffèrent par leurs mécanismes de syn-
chronisation et de communication.
Pourquoi est-ce qu’on s’intéresse à la programmation concurrente ?
– Certains algorithmes sont plus simples quand vus comme des algorithmes concur-
rents (par exemple le crible d’Eratosthène, voir le TD)
– On essaye de parallèliser des programmes qui exigent beaucoup de calcul, et
de les distribuer sur plusieurs machines (Seti@home, ExtremeWeb, création de
bandes animées, etc.)
– Il y a des problèmes qui sont concurrents par leur nature (communication entre
des ordinateurs, problèmes de routage dans des réseaux, etc.)

16 Dans le monde fonctionnel


En OCaml, quand une expression est évaluée elle l’est complètement. Par consé-
quence, une expression comme
let integers_from_n n = n :: (integers_from_n (n+1))
n’est pas utile en OCaml puisqu’elle ne terminera pas. Pourtant, il y a des problèmes
qui sont naturellement de la forme producteur - consommateur :
– une partie du programme consiste à engendrer un flot de donnés
– une autre partie du programme consiste à consommer ce flot de donnés
(Exemple : combinaison d’analyse lexicale (producteur d’un flot de token) et syn-
taxique (consommateur d’un flot de token).
Il y a une famille de langages fonctionnels qui permettent de créer une structure
qu’à demande : ce sont des langages à évaluation retardée, aussi appelée évaluation
paresseuse (angl. : lazy evaluation).
Représentants principals de cette famille : M IRANDA et H ASKELL. Dans ces lan-
gages, on peut définir par exemple une fonction comme la fonction integers_from_n
au dessus, puis définir une autre fonction consumer qui prend une liste en argument,
et évaluer
consumer (integers_from_n 1)
Au début l’évaluation de l’expression (integers_from_n 1) est en suspense.
Chaque fois que la fonction consumer essaye d’appliquer une fonction d’accès (hd,
tl) à son argument, l’évaluation de l’expression (integers_from_n 1) est «
réveillée ». Quand l’élément suivant de la liste est engendré l’évaluation de cette ex-
pression est de nouveau suspendue.
On peut simuler ce mécanisme d’évaluation retardée en OCaml .

54
17 Modèle à mémoire répartie
Modèle des processus en UNIX : un programme en cours d’exécution. Moyens de
communications entre processus :
1. par entrées-sorties d’un fichier
2. par des tubes (synchronisation implicite)
3. par des signaux (plutôt primitif)
En UNIX, les processus sont organisés dans un arbre. Au démarrage du système,
un processus init est créé. Puis, un processus peut créer des nouveaux processus. Tout
processus est identifié par son numéro, appelé PID (Process IDentifier).
Au niveau shell, par exemple, il y a trois cas de figure pour le lancement d’un
processus :
1. le processus père est en attente sur la fin d’exécution du processus fils : ocamlc p.ml
2. les deux processus sont indépendants et peuvent s’exécuter concurremment (lan-
cement d’un processus en plan arrière : ocamlc p.ml &)
3. le processus lancé remplace le processus père qui disparaît (appel de système
exec, comme dans les script qui commencent sur #!/bin/sh)
À un niveau plus bas, les processus sont crées à l’aide de l’appel de système fork.
Il crée deux copies du processus courant, qui se distinguent seulement par le résultat
qu’ils obtiennent à l’appel du fork : Le processus qui vient de naître reçoit le résultat 0,
et le processus père reçoit comme résultat le PID du processus fils.
Voir la demo fork :
open Unix;;

let f () =
let pid = Unix.fork ()
in
if pid=0 then (* -- Code du fils *)
Printf.printf "je suis le fils : %d\n" (Unix.getpid ())
else (* -- Code du pere *)
Printf.printf "je suis le père : %d du fils : %d\n" (Unix.getpid ()) pid ;;

f () ;;

Communications par tubes


Les tubes ou tuyaux (angl. : pipe) sont des canaux de communications anonymes.
Un processus peut écrire dans un tube et lire d’un tube comme il peut écrire sur un
fichier et lire d’un fichier.
Les tubes entraînent une synchronisation implicite de processus qui y accèdent :
un processus qui essaie de lire d’un tube vide, ou qui essaie d’écrire sur un tube plein,
est suspendu. De même, un processus qui essaie d’écrire sur un tube dont la sortie est
fermée (c.-à-d., il y a aucun processus qui a ouvert ce tube à la lecture) reçoit un signal.
Voir la demo pipes :

55
open Unix;;

let camlpipe () =
let (fd_out, fd_in) = Unix.pipe ()
in (Unix.in_channel_of_descr fd_out, Unix.out_channel_of_descr fd_in);;

let sortie, entree = camlpipe();;

(* écrire son PID sur le canal entree *)


let write_pid entree =
try
output_string entree ( "(" ^ (string_of_int (Unix.getpid ())) ^ ")\n" );
close_out entree
with
Unix.Unix_error(n,f,arg) ->
Printf.printf "%s(%s) : %s\n" f arg (Unix.error_message n) ;;

match Unix.fork () with


0 -> for i=0 to 5 do
match Unix.fork() with
0 -> write_pid entree ; exit 0
| _ -> ()
done ;
close_out entree
| _ -> close_out entree;
print_string "Mes petits fils sont : ";
try
while true do
print_string (input_line sortie)
done
with
End_of_file -> begin
close_in sortie;
print_newline ()
end;;

La communication par tubes anonymes est donc réservée aux processus qui sont en
relation de parenté. Par contre, les UNIX modernes fournissent aussi des tubes nom-
més (aussi appelé fifo) qui permettent à deux processus de communiquer par un tube
seulement en sachant le nom du tube.

18 Modèle à mémoire partagée


Modèle des processus légers (angl : threads). Un processus léger n’est qu’une struc-
ture de contrôle (essentiellement la position actuelle dans le programme). La création
et l’administration sont beaucoup moins chers pour les processus légers que pour les
processus.

56
En OCaml, un processus léger est crée à l’aide de la fonction
Thread.create : (’a -> ’b) -> ’a -> Thread.t
Le premier argument, de type α → β, correspond à la fonction exécutée par le
processus créé ; le second argument, de type α, est l’argument attendu par la fonction
exécutée ; le résultat de l’appel est le descripteur associé au processus. Le processus
ainsi créé est détruit automatiquement lorsque la fonction associée termine.
Connaissant son descripteur, on peut demander l’exécution d’un processus et en
attendre la fin en utilisant la fonction join.
Thread.join : Thread.t -> unit
(voir la demo)
let n =ref 1;;

open Thread;;

let f_proc1 () =
for i=0 to 10 do Printf.printf "(%d)" i; flush stdout done;
print_newline() ;;

let t1 = Thread.create f_proc1 () ;;

(* Thread.delay 1.0;; *)
Thread.join t1;;

On peut tuer un thread en utilisant la fonction Thread.kill : Thread.t -> unit.


(voir la demo)
let n = ref 0;;

let f_proc1 () =
while true do incr n done;;

let f_proc2 d =
n := 0 ;
let t2 = Thread.create f_proc1 ()
in Thread.delay d ;
Thread.kill t2 ;;

let t1 = Thread.create f_proc2 0.25


in
Thread.join t1;
Printf.printf "n = %d\n" !n ;;

18.1 Exclusion mutuelle


à l’aide des verrous (angl. : lock.) En OCaml, il y a les fonctions suivantes :

57
Mutex.create : unit -> Mutex.t
Mutex.lock : Mutex.t -> unit
Mutex.unlock : Mutex.t -> unit
Mutex.try_lock : Mutex.t -> bool
La fonction lock bloque dans le cas où le verrou est pris, tandis que la fonction
try_lock ne bloque pas (le résultat est false quand le verrou est pris). (Voir la démo)
open Thread;;
open Graphics;;

let numberofphils = 5;;


let right i = (i+1) mod numberofphils;;

let b =
let b0 = Array.create numberofphils (Mutex.create()) in
for i=1 to (numberofphils-1) do b0.(i) <- Mutex.create() done;
b0 ;;

let mediter = Thread.delay


and manger = Thread.delay ;;

let philosophe i =
while true do
(* mediter 3.; *)
mediter (Random.float 2.);
Mutex.lock b.(i);
Printf.printf "Le philosophe (%d) prend sa baguette gauche" i ;
Printf.printf " et médite encore un peu\n";
flush stdout;
mediter 0.2;
Mutex.lock b.(right i);
Printf.printf "Le philosophe (%d) prend sa baguette droite\n" i;
flush stdout;
manger 0.5;
Mutex.unlock b.(i);
Printf.printf "Le philosophe (%d) repose sa baguette gauche" i;
Printf.printf " et commence déjà à méditer\n";
flush stdout;
mediter 0.15;
Mutex.unlock b.(right i);
Printf.printf "Le philosophe (%d) repose sa baguette droite\n" i;
flush stdout
done ;;

for i=0 to numberofphils-1 do ignore (Thread.create philosophe i) done ;


while true do Thread.delay 5. done ;;

58
Cette solution a un grand désavantage : on risque un inter-blocage (angl. : deadlock),
c.-à-d. une situation où aucun processus ne peut avancer. Sur l’exemple (et avec ocaml
3.06) l’inter-blocage arrive immédiatement puisque toutes les philosophes prennent la
baguette à leur gauche.
En pratique, la randomisation du temps de méditation (comme indiquée dans le
commentaire) semble résoudre le problème d’inter-blocage, mais le risque est toujours
là.
Exercice: Une solution au problème d’inter-blocage dans la solution naïve du pro-
blème des philosophes est de faire prendre chaque philosophe un ticket quand il a faim.
Si un philosophe et ses deux voisins ont faim alors il ne prend les baguettes que quand
le numéro sur son ticket est plus petit que le numéro sur les tickets de ses voisins.
Indiquer les modifications nécessaires au programme naïve pour réaliser cette so-
lution.
Un autre exemple classique est le modèle des producteurs et consommateurs : Un
groupe de processus, désignés comme les producteurs, est chargé d’emmagasiner des
données dans une file d’attente ; un second groupe, les consommateurs, est chargé de
les déstocker. Chaque intervenant exclut les autres. Une première réalisation utilise les
verrous (voir la demo)
(* la file d’attente avec son verrou *)
let f = Queue.create () and m = Mutex.create () ;;

(* la fonction suivante simule la production *)


let produire producteur pcounter duree =
incr pcounter ;
Thread.delay duree ;
Printf.printf "Le producteur (%d) a produit %d\n" producteur !pcounter ;
flush stdout ;;

(* mettre un produit dans la file d’attente *)


let stocker producteur pcounter =
Mutex.lock m ;
Queue.add (producteur,!pcounter) f ;
Printf.printf "Le producteur (%d) a ajouté son %d-ième produit\n"
producteur !pcounter ;
flush stdout ;
Mutex.unlock m ;;

(* un processus producteur *)
let producteur i =
let p = ref 0 and d = Random.float 2.
in while true do
produire i p d ;
stocker i p ;
Thread.delay (Random.float 2.5)
done ;;

(* un processus consommateur *)
let consommateur i =

59
while true do
Mutex.lock m ;
( try
let ip, p = Queue.take f
in Printf.printf "Le consommateur(%d) " i ;
Printf.printf "a retiré le produit (%d,%d)\n" ip p ;
flush stdout ;
with
Queue.Empty ->
Printf.printf "Le consommateur(%d) " i ;
print_string "est reparti les mains vides\n" ) ;
Mutex.unlock m ;
Thread.delay (Random.float 2.5)
done ;;

(* tous ensemble *)
for i = 0 to 3 do
ignore (Thread.create producteur i);
ignore (Thread.create consommateur i)
done ;
while true do Thread.delay 5. done ;;

18.2 Attente et synchronisation


La solution de la section précédente présente le désavantage que les consommateurs
attendent activement que la file soit remplie, ce qui est un gaspillage de ressources
de calcul. Une meilleure solution est d’utiliser une synchronisation entre processus,
comme réalisées par le module Condition de OCaml :
create : unit → Condition.t crée un nouveau signal.
signal : Condition.t → unit réveille l’un des processus en attente du signal.
broadcast : Condition.t -> unit réveille l’ensemble des processus en attente du si-
gnal.
wait : Condition.t -> Mutex.t -> unit suspend le processus appelant sur le signal passé
en premier argument. Le second argument est un verrou. Au début de l’exécution
de wait, le verrou est libéré. Quand la fonction wait termine (parce qu’elle à
reçu le signal attendu) elle reprend le verrou.
(voir la demo)
(* la file d’attente avec son verrou, et sa condition *)
let f = Queue.create ()
and m = Mutex.create ()
and c = Condition.create();;

(* la fonction suivante simule la production *)


let produire producteur pcounter duree =
incr pcounter ;

60
Thread.delay duree ;
Printf.printf "Le producteur (%d) a produit %d\n" producteur !pcounter ;
flush stdout ;;

let stocker i p =
Mutex.lock m ;
Queue.add (i,!p) f ;
Printf.printf "Le producteur (%d) a ajoute son %d-ième produit\n" i !p ;
flush stdout ;
Condition.signal c ;
Mutex.unlock m ;;

let producteur i =
let p = ref 0 in
let d = Random.float 2.
in while true do
produire i p d;
stocker i p;
Thread.delay (Random.float 2.5)
done ;;

let attendre i =
Mutex.lock m ;
while Queue.length f = 0 do
Printf.printf "Le consommateur (%d) attend\n" i ;
Condition.wait c m
done;;

let emporter i =
try
let ip, p = Queue.take f in
Printf.printf "Le consommateur (%d) " i ;
Printf.printf "emporte le produit (%d, %d)\n" ip p ;
flush stdout ;
Mutex.unlock m
with
Queue.Empty ->
Printf.printf
"consommateur (%d) essaye de consommer d’un file vide\n" i;
flush stdout;
exit 1
;;

let consommateur i =
while true do
attendre i;
emporter i;
Thread.delay (Random.float 2.5)

61
done ;;

(* tous ensemble *)
for i = 0 to 3 do
ignore (Thread.create producteur i);
ignore (Thread.create consommateur i)
done ;
while true do Thread.delay 5. done ;;

Un autre mécanisme de synchronisation est fourni par les sémaphores (voir le cours
de Systèmes). Un sémaphore est une variable entière s ne pouvant prendre que des
valeurs positives (ou nulles). Une fois s initialisée, les seules opérations admises sont
wait(s) et signal(s), notées respectivement P(s) et V(s) :
– P(s) : si s > 0 alors s := s − 1, sinon l’exécution du processus ayant appelé
wait(s) est suspendue ;
– V(s) : s := s+1, et si un processus a été suspendu lors d’une exécution antérieure
d’un P(s) alors le réveiller.
Exercice: Écrire un module OCaml qui défini un type semaphore, avec les trois
fonctions d’initialisation, p, et v.

62