 
 
 
 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:
- 
 define a common ``account manager'' structure by application of
the FManager functor;
-  declare two signatures listing only the functions accessible to the
bank or to the customer;
-  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.
 
 
 
