 
 
 
 Exemple : gestion de comptes bancaires
Nous terminons ce chapitre par la réalisation d'un petit exemple
illustrant les principaux traits de l'utilisation d'une programmation
modulaire : abstraction de type ; diverses vues d'un module ;
réutilisation de code à l'aide des foncteurs.
Le but de cet exemple est de fournir deux modules de gestion d'un
compte bancaire. L'un est destiné au banquier et l'autre au client.
Le principe est de réaliser un module paramétré général fournissant
toutes les fonctionnalités souhaitables de gestion puis de l'appliquer
à certains paramètres en le contraignant avec la signature
correspondant à sa destination finale : le banquier ou le client.
 Organisation de l'application
 
Figure 14.1 : graphe de dépendances des modules
 
 
Les deux modules finals BGestion et CGestion sont
obtenus par contrainte sur la signature du module Gestion. Ce
dernier est obtenu par application du foncteur FGestion aux
modules Compte, Date et deux autres modules
construits également par application des foncteurs FHisto
et FReleve. La figure 14.1 illustre ces
dépendances. 
 Signatures des modules paramètres
Un module de gestion de compte bancaire est paramétré par quatre
autres modules dont nous donnons et commentons ci-dessous les signatures.
 Le compte lui-même
 Ce module fournit essentiellement les
opération de calcul sur le contenu du compte.
# module type COMPTE = sig 
   type t
   exception OperationImpossible
   val creer : float -> float -> t
   val depot : float -> t -> unit
   val retrait : float -> t -> unit
   val solde : t -> float
 end ;;
Cet ensemble de fonctions fournit les opérations minimales de gestion
du contenu du compte. L'opération de création prend en argument
le solde initial du compte et la valeur du découvert
autorisé. L'opération de retrait peut déclencher l'exception
OperationImpossible. 
 Des clés ordonnées
 Les opérations seront
enregistrées selon un historique décrit au paragraphe
suivant. On associera une clé à chaque enregistrement. Les
fonctions de gestion des clés sont données par la signature :
# module type OCLE =
   sig
     type t
     val creer : unit -> t
     val of_string : string -> t
     val to_string : t -> string
     val eq : t -> t -> bool
     val lt : t -> t -> bool
     val gt : t -> t -> bool
   end ;;
La fonction creer permet d'engendrer une clé supposée
unique. Les fonction of_string et to_string sont
des opérations de conversion de et vers les chaînes de
caractères. Les trois autres fonctions sont des fonctions de
comparaison. 
 Historique
 Pour garder l'historique de gestion d'un
compte, on se donne l'ensemble de données abstraites et de fonctions
suivant :
# module type HISTO = 
   sig
     type tcle
     type tinfo
     type t
     val creer : unit -> t
     val add : tcle -> tinfo -> t -> unit
     val nth : int -> t -> tcle*tinfo
     val get : (tcle -> bool) -> t -> (tcle*tinfo) list 
   end ;;
Nous laissons pour l'instant indéterminé la nature des clés
d'enregistrement (type tcle) la nature des informations (type
tinfo) et la structure de stockage (type t). On
supposera que l'ajout de nouvelles informations (fonction add)
est séquentiel. Le module fournit deux fonctions d'accès aux
données archivées : un accès selon leur ordre d'enregistrement
(fonction nth) et un accès selon un critère défini sur
les clés (fonction get).
 Les relevés de compte
 Le dernier paramètre du module de
gestion fournit deux fonctions d'édition d'un relevé de compte :
# module type RELEVE =
    sig
      type tdata
      type tinfo
      val editB : tdata -> tinfo
      val editC : tdata -> tinfo
    end ;;
On laisse abstrait le type de données à traiter (tdata)
ainsi que le type des informations extraites (tinfo).
 Module général paramétré de gestion
Munis des seules informations que fournissent les signatures
ci-dessus, nous pouvons donner la structure du foncteur général de
gestion d'un compte :
# module FGestion =
  functor (C:COMPTE) ->
  functor (K:OCLE) ->
  functor (H:HISTO with type tcle=K.t and type tinfo=float) ->
  functor (R:RELEVE with type tdata=H.t and type tinfo=(H.tcle*H.tinfo) list) ->
    struct
      type t = { mutable c : C.t; mutable h : H.t }
      let creer s d = { c = C.creer s d; h = H.creer() }
      let depot s g = C.depot s g.c ; H.add (K.creer()) s g.h
      let retrait s g =  C.retrait s g.c ; H.add (K.creer()) (-.s) g.h
      let solde g = C.solde g.c
      let releve edit g = 
        let f (d,i) = (K.to_string d) ^ ":" ^ (string_of_float i)
        in List.map f (edit g.h)
      let releveB = releve R.editB
      let releveC = releve R.editC
    end ;; 
module FGestion :
  functor(C : COMPTE) ->
    functor(K : OCLE) ->
      functor
        (H : sig
               type tcle = K.t
               and tinfo = float
               and t
               val creer : unit -> t
               val add : tcle -> tinfo -> t -> unit
               val nth : int -> t -> tcle * tinfo
               val get : (tcle -> bool) -> t -> (tcle * tinfo) list
             end) ->
        functor
          (R : sig
                 type tdata = H.t
                 and tinfo = (H.tcle * H.tinfo) list
                 val editB : tdata -> tinfo
                 val editC : tdata -> tinfo
               end) ->
          sig
            type t = { mutable c: C.t; mutable h: H.t }
            val creer : float -> float -> t
            val depot : H.tinfo -> t -> unit
            val retrait : float -> t -> unit
            val solde : t -> float
            val releve : (H.t -> (K.t * float) list) -> t -> string list
            val releveB : t -> string list
            val releveC : t -> string list
          end
 Précision et partage de types
 La contrainte de type
appliquée au paramètre H du foncteur FGestion
précise que les clés de l'historique sont celles fournies par le
paramètre K et que les informations stockées sont simplement
des flottants (le montant des transactions). La contrainte de type
appliquée au paramètre R indique que les informations
traitées par le relevé proviennent de l'historique (paramètre
H). 
La signature inférée du foncteur FGestion
prend en compte les contraintes de type dans la signature
inférée des arguments.
On définit le type d'un compte comme le contenu du compte
(paramètre C) et son historique. 
 Opérations
 Il est important de remarquer ici que toutes
 les opérations de ce foncteur sont définies en terme de
 fonctions fournies par les modules paramètres. 
 Les opérations de création, retrait et dépot affectent le
 contenu du compte et son historique. Les deux opérations de
 consultation sont la lecture du contenu du compte (fonction
 solde) et la lecture d'une partie de l'historique (fonction
 releve). 
 Définition des paramètres
 Il faut, avant de créer
 les modules finals, définir le contenu des paramètres du foncteur
 FGestion. 
 Contenu
Le contenu d'un compte est essentiellement un flottant représentant
le solde courant. On rajoute l'information d'autorisation de
découvert qui sert à contrôler l'opération de retrait.
# module Compte:COMPTE = 
  struct
   type t = { mutable solde:float; decouvert:float }
   exception OperationImpossible
   let creer s d = { solde=s; decouvert=(-.d) }
   let depot s c =  c.solde <- c.solde+.s
   let solde c = c.solde
   let retrait s c = 
    let ss = c.solde -. s in
     if ss < c.decouvert then raise OperationImpossible
     else c.solde <- ss
  end ;;
module Compte : COMPTE
 Choix d'une clé
 On décide que la clé d'enregistrement
 est simplement la date de la transaction exprimée en flottant telle
 que la fournit la fonction time du module Unix.
# module Date:OCLE =
  struct
   type t = float
   let creer() = Unix.time()
   let of_string = float_of_string
   let to_string = string_of_float
   let eq = (=)
   let lt = (<)
   let gt = (>)
  end ;;
module Date : OCLE
 L'historique
 Nous l'avons vu, l'historique d'un compte
 dépend du type de clé choisi, c'est pourquoi nous définissons
 un foncteur prenant en argument un module fournissant le type des
 clés, ce qui permet de définir le type des clés
 d'enregistrement.
# module FHisto (K:OCLE) =
  struct
   type tcle = K.t
   type tinfo = float
   type t = { mutable content : (tcle*tinfo) list }
   let creer() = { content = [] }
   let add c i h = h.content <- (c,i)::h.content
   let nth i h = List.nth h.content i
   let get f h = List.filter (fun (c,_) -> (f c)) h.content
  end ;; 
module FHisto :
  functor(K : OCLE) ->
    sig
      type tcle = K.t
      and tinfo = float
      and t = { mutable content: (tcle * tinfo) list }
      val creer : unit -> t
      val add : tcle -> tinfo -> t -> unit
      val nth : int -> t -> tcle * tinfo
      val get : (tcle -> bool) -> t -> (tcle * tinfo) list
    end
Remarquons que le type des informations doit être cohérent avec
celui donné lors de la définition du foncteur de gestion.
 Les relevés
 Nous définissons deux types de
relevé. Le premier (editB) donne les cinq dernières
transactions et est destiné au banquier ; le second (editC)
donne les transactions effectuées les dix derniers jours à partir
de la date courante et est destiné au client.
# module FReleve (K:OCLE) (H:HISTO with type tcle=K.t) =
  struct
   type tdata = H.t
   type tinfo = (H.tcle*H.tinfo) list
   let editB h =
    List.map (fun i -> H.nth i h) [0;1;2;3;4]
   let editC h =
    let c0 = K.of_string (string_of_float ((Unix.time()) -. 864000.)) in
    let f = K.lt c0 in
     H.get f h
  end ;;
module FReleve :
  functor(K : OCLE) ->
    functor
      (H : sig
             type tcle = K.t
             and tinfo
             and t
             val creer : unit -> t
             val add : tcle -> tinfo -> t -> unit
             val nth : int -> t -> tcle * tinfo
             val get : (tcle -> bool) -> t -> (tcle * tinfo) list
           end) ->
      sig
        type tdata = H.t
        and tinfo = (H.tcle * H.tinfo) list
        val editB : H.t -> (H.tcle * H.tinfo) list
        val editC : H.t -> (H.tcle * H.tinfo) list
      end
Remarquons que pour définir le relevé décadaire, il nous faut
connaître précisément l'implantation des clés
utilisées. C'est sans doute là une entorse au principe des types
abstraits. Cependant la clé correspondant à une date antérieure
de dix jours que nous utilisons est obtenue à partir de sa
représentation textuelle par appel à la fonction
of_string et non pas par calcul direct de la représentation
interne de cette date (même si notre exemple est trop pauvre pour
rendre ce distingo très manifeste).
 Les modules finals
 Pour construire les modules
GBanque et GClient respectivement destinés au banquier et 
au client nous procédons comme suit :
- 
 définition d'une structure par application du foncteur
FGestion aux paramètres.
-  déclaration d'une signature indiquant les fonctions
accessibles dans chacun des cas.
-  définition du module final par contrainte de la structure
obtenue en 1 avec la signature déclarée en 2.
# module Gestion =
  FGestion (Compte) 
           (Date) 
           (FHisto(Date)) 
           (FReleve (Date) (FHisto(Date))) ;;
module Gestion :
  sig
    type t =
      FGestion(Compte)(Date)(FHisto(Date))(FReleve(Date)(FHisto(Date))).t =
      { mutable c: Compte.t;
        mutable h: FHisto(Date).t }
    val creer : float -> float -> t
    val depot : FHisto(Date).tinfo -> t -> unit
    val retrait : float -> t -> unit
    val solde : t -> float
    val releve :
      (FHisto(Date).t -> (Date.t * float) list) -> t -> string list
    val releveB : t -> string list
    val releveC : t -> string list
  end
# module type GESTION_BANQUE = 
   sig
    type t
    val creer : float -> float -> t
    val depot : float -> t -> unit
    val retrait : float -> t -> unit
    val solde : t -> float
    val releveB : t ->  string list
   end ;;
# module GBanque = (Gestion:GESTION_BANQUE with type t=Gestion.t) ;;
module GBanque :
  sig
    type t = Gestion.t
    val creer : float -> float -> t
    val depot : float -> t -> unit
    val retrait : float -> t -> unit
    val solde : t -> float
    val releveB : t -> string list
  end
# module type GESTION_CLIENT =
   sig
    type t
    val depot : float -> t -> unit
    val retrait : float -> t -> unit
    val solde : t -> float
    val releveC : t ->  string list
   end ;;
# module GClient = (Gestion:GESTION_CLIENT with type t=Gestion.t) ;; 
module GClient :
  sig
    type t = Gestion.t
    val depot : float -> t -> unit
    val retrait : float -> t -> unit
    val solde : t -> float
    val releveC : t -> string list
  end
Pour que les comptes créés par un banquier puissent être manipulés
par un client nous avons utilisé la contrainte de type
Gestion.t dans la définition des modules GBanque et 
GClient. 
 
 
 
