Author: Christian.Queinnec@lip6.fr

## Extended Example: Managing Bank Accounts

We conclude this chapter by an example illustrating the main aspects of modular programming: type abstraction, multiple views of a module, and functor-based code reuse.
The goal of this example is to provide two modules for managing a bank account. One is intended to be used by the bank, and the other by the customer. The approach is to implement a general-purpose parameterized functor providing all the needed operations, then apply it twice to the correct parameters, constraining it by the signature corresponding to its final user: the bank or the customer.

### Organization of the Program

> Figure 14.1: Modules dependency graph.

The two end modules `BManager` and `CManager` are obtained by constraining the module `Manager`. The latter is obtained by applying the functor `FManager` to the modules `Account`, `Date` and two additional modules built by application of the functors `FLog` and `FStatement`. Figure 14.1 illustrates these dependencies.

### Signatures for the Module Parameters

The module for account management is parameterized by four other modules, whose signatures we now detail.

##### The bank account.

This module provides the basic operations on the contents of the account.

    # module type ACCOUNT = sig
       type t
       exception BadOperation
       val create : float -> float -> t
       val deposit : float -> t -> unit
       val withdraw : float -> t -> unit
       val balance : t -> float
     end ;;

This set of functions provide the minimal operations on an account. The creation operation takes as arguments the initial balance and the maximal overdraft allowed. Excessive withdrawals may raise the `BadOperation` exception.

##### Ordered keys.

Operations are recorded in an operation log described in the next paragraph. Each log entry is identified by a key. Key management functions are described by the following signature:

    # module type OKEY =
       sig
         type t
         val create : 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 ;;

The `create` function returns a new, unique key. The functions `of_string` and `to_string` convert between keys and character strings. The three remaining functions are key comparison functions.

##### History.

Logs of operations performed on an account are represented by the following abstract types and functions:

    # module type LOG =
       sig
         type tkey
         type tinfo
         type t
         val create : unit -> t
         val add : tkey -> tinfo -> t -> unit
         val nth : int -> t -> tkey*tinfo
         val get : (tkey -> bool) -> t -> (tkey*tinfo) list
       end ;;

We keep unspecified for now the types of the log keys (type tkey) and of the associated data (type tinfo), as well as the data structure for storing logs (type t). We assume that new informations added with the `add` function are kept in sequence. Two access functions are provided: access by position in the log (function `nth`) and access following a search predicate on keys (function `get`).

##### Account statements.

The last parameter of the manager module provides two functions for editing a statement for an account:

    # module type STATEMENT =
        sig
          type tdata
          type tinfo
          val editB : tdata -> tinfo
          val editC : tdata -> tinfo
        end ;;

We leave abstract the type of data to process (tdata) as well as the type of informations extracted from the data (tinfo).

### The Parameterized Module for Managing Accounts

Using only the information provided by the signatures above, we now define the general-purpose functor for managing accounts.

    # module FManager =
      functor (C:ACCOUNT) ->
      functor (K:OKEY) ->
      functor (L:LOG with type tkey=K.t and type tinfo=float) ->
      functor (S:STATEMENT with type tdata=L.t and type tinfo
              = (L.tkey*L.tinfo) list) ->
        struct
          type t = { accnt : C.t; log : L.t }
          let create s d = { accnt = C.create s d; log = L.create() }
          let deposit s g =
            C.deposit s g.accnt ; L.add (K.create()) s g.log
          let withdraw s g =
            C.withdraw s g.accnt ; L.add (K.create()) (-.s) g.log
          let balance g = C.balance g.accnt
          let statement edit g =
            let f (d,i) = (K.to_string d) ^ ":" ^ (string_of_float i)
            in List.map f (edit g.log)
          let statementB = statement S.editB
          let statementC = statement S.editC
        end ;;
    module FManager :
      functor(C : ACCOUNT) ->
        functor(K : OKEY) ->
          functor
            (L : sig
                   type tkey = K.t
                   and tinfo = float
                   and t
                   val create : unit -> t
                   val add : tkey -> tinfo -> t -> unit
                   val nth : int -> t -> tkey * tinfo
                   val get : (tkey -> bool) -> t -> (tkey * tinfo) list
                 end) ->
            functor
              (S : sig
                     type tdata = L.t
                     and tinfo = (L.tkey * L.tinfo) list
                     val editB : tdata -> tinfo
                     val editC : tdata -> tinfo
                   end) ->
              sig
                type t = { accnt: C.t; log: L.t }
                val create : float -> float -> t
                val deposit : L.tinfo -> t -> unit
                val withdraw : float -> t -> unit
                val balance : t -> float
                val statement : (L.t -> (K.t * float) list) -> t -> string list
                val statementB : t -> string list
                val statementC : t -> string list
              end

##### Sharing between types.

The type constraint over the parameter `L` of the `FManager` functor indicates that the keys of the log are those provided by the `K` parameter, and that the informations stored in the log are floating-point numbers (the transaction amounts). The type constraint over the `S` parameter indicates that the informations contained in the statement come from the log (the `L` parameter). The signature inferred for the `FManager` functor reflects the type sharing constraints in the inferred signatures for the functor parameters.
The type `t` in the result of `FManager` is a pair of an account (`C.t`) and its transaction log.

##### Operations.

All operations defined in this functor are defined in terms of lower-level functions provided by the module parameters. The creation, deposit and withdrawal operations affect the contents of the account and add an entry in its transaction log. The other functions return the account balance and edit statements.

### Implementing the Parameters

Before building the end modules, we must first implement the parameters to the `FManager` module.

##### Accounts.

The data structure for an account is composed of a float representing the current balance, plus the maximum overdraft allowed. The latter is used to check withdrawals.

    # module Account:ACCOUNT =
      struct
       type t = { mutable balance:float; overdraft:float }
       exception BadOperation
       let create b o = { balance=b; overdraft=(-. o) }
       let deposit s c =  c.balance <- c.balance +. s
       let balance c = c.balance
       let withdraw s c =
        let ss = c.balance -. s in
         if ss < c.overdraft then raise BadOperation
         else c.balance <- ss
      end ;;
    module Account : ACCOUNT

##### Choosing log keys.

We decide that keys for transaction logs should be the date of the transaction, expressed as a floating-point number as returned by the `time` function from module `Unix`.

    # module Date:OKEY =
      struct
       type t = float
       let create() = Unix.time()
       let of_string = float_of_string
       let to_string = string_of_float
       let eq = (=)
       let lt = (<)
       let gt = (>)
      end ;;
    module Date : OKEY

##### The log.

The transaction log depends on a particular choice of log keys. Hence we define logs as a functor parameterized by a key structure.

    # module FLog (K:OKEY) =
      struct
       type tkey = K.t
       type tinfo = float
       type t = { mutable contents : (tkey*tinfo) list }
       let create() = { contents = [] }
       let add c i l = l.contents <- (c,i) :: l.contents
       let nth i l = List.nth l.contents i
       let get f l = List.filter (fun (c,_) -> (f c)) l.contents
      end ;;
    module FLog :
      functor(K : OKEY) ->
        sig
          type tkey = K.t
          and tinfo = float
          and t = { mutable contents: (tkey * tinfo) list }
          val create : unit -> t
          val add : tkey -> tinfo -> t -> unit
          val nth : int -> t -> tkey * tinfo
          val get : (tkey -> bool) -> t -> (tkey * tinfo) list
        end

Notice that the type of informations stored in log entries must be consistent with the type used in the account manager functor.

##### Statements.

We define two functions for editing statements. The first (`editB`) lists the five most recent transactions, and is intended for the bank; the second (`editC`) lists all transactions performed during the last 10 days, and is intended for the customer.

    # module FStatement (K:OKEY) (L:LOG with type tkey=K.t) =
      struct
       type tdata = L.t
       type tinfo = (L.tkey*L.tinfo) list
       let editB h =
        List.map (fun i -> L.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
         L.get f h
      end ;;
    module FStatement :
      functor(K : OKEY) ->
        functor
          (L : sig
                 type tkey = K.t
                 and tinfo
                 and t
                 val create : unit -> t
                 val add : tkey -> tinfo -> t -> unit
                 val nth : int -> t -> tkey * tinfo
                 val get : (tkey -> bool) -> t -> (tkey * tinfo) list
               end) ->
          sig
            type tdata = L.t
            and tinfo = (L.tkey * L.tinfo) list
            val editB : L.t -> (L.tkey * L.tinfo) list
            val editC : L.t -> (L.tkey * L.tinfo) list
          end

In order to define the 10-day statement, we need to know exactly the implementation of keys as floats. This arguably goes against the principles of type abstraction. However, the key corresponding to ten days ago is obtained from its string representation by calling the `K.of_string` function, instead of directly computing the internal representation of this date. (Our example is probably too simple to make this subtle distinction obvious.)

##### End modules.

To build the modules `MBank` and `MCustomer`, for use by the bank and the customer respectively, we proceed as follows:

1. define a common ``account manager'' structure by application of the `FManager` functor;
2. declare two signatures listing only the functions accessible to the bank or to the customer;
3. constrain the structure obtained in 1 with the signatures declared in 2.

    # module Manager =
      FManager (Account)
               (Date)
               (FLog(Date))
               (FStatement (Date) (FLog(Date))) ;;
    module Manager :
      sig
        type t =
          FManager(Account)(Date)(FLog(Date))(FStatement(Date)(FLog(Date))).t =
          { accnt: Account.t;
            log: FLog(Date).t }
        val create : float -> float -> t
        val deposit : FLog(Date).tinfo -> t -> unit
        val withdraw : float -> t -> unit
        val balance : t -> float
        val statement :
          (FLog(Date).t -> (Date.t * float) list) -> t -> string list
        val statementB : t -> string list
        val statementC : t -> string list
      end

    # module type MANAGER_BANK =
       sig
        type t
        val create : float -> float -> t
        val deposit : float -> t -> unit
        val withdraw : float -> t -> unit
        val balance : t -> float
        val statementB : t ->  string list
       end ;;

    # module MBank = (Manager:MANAGER_BANK with type t=Manager.t) ;;
    module MBank :
      sig
        type t = Manager.t
        val create : float -> float -> t
        val deposit : float -> t -> unit
        val withdraw : float -> t -> unit
        val balance : t -> float
        val statementB : t -> string list
      end

    # module type MANAGER_CUSTOMER =
       sig
        type t
        val deposit : float -> t -> unit
        val withdraw : float -> t -> unit
        val balance : t -> float
        val statementC : t ->  string list
       end ;;

    # module MCustomer = (Manager:MANAGER_CUSTOMER with type t=Manager.t) ;;
    module MCustomer :
      sig
        type t = Manager.t
        val deposit : float -> t -> unit
        val withdraw : float -> t -> unit
        val balance : t -> float
        val statementC : t -> string list
      end

In order for accounts created by the bank to be usable by clients, we added the type constraint on Manager.t in the definition of the `MBank` and `MCustomer` structures, to ensure that their t type components are compatible.