Previous Contents Next

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.








Previous Contents Next