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())
-.
8
6
4
0
0
0
.
))
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.