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.