Author: Christian.Queinnec@lip6.fr

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

1. définition d'une structure par application du foncteur `FGestion` aux paramètres.
2. déclaration d'une signature indiquant les fonctions accessibles dans chacun des cas.
3. 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`.