Chapter 6 Advanced examples with classes and modules

(Chapter written by Didier Rémy)

In this chapter, we show some larger examples using objects, classesand modules. We review many of the object features simultaneously onthe example of a bank account. We show how modules taken from thestandard library can be expressed as classes. Lastly, we describe aprogramming pattern known as virtual types through the exampleof window managers.

6.1 Extended example: bank accounts

In this section, we illustrate most aspects of Object and inheritanceby refining, debugging, and specializing the followinginitial naive definition of a simple bank account. (We reuse themodule Euro defined at the end of chapter 3.)

  1. let euro = new Euro.c;;
  2. val euro : float -> Euro.c = <fun>
  1. let zero = euro 0.;;
  2. val zero : Euro.c = <obj>
  1. let neg x = x#times (-1.);;
  2. val neg : < times : float -> 'a; .. > -> 'a = <fun>
  1. class account =
  2. object
  3. val mutable balance = zero
  4. method balance = balance
  5. method deposit x = balance <- balance # plus x
  6. method withdraw x =
  7. if x#leq balance then (balance <- balance # plus (neg x); x) else zero
  8. end;;
  9. class account :
  10. object
  11. val mutable balance : Euro.c
  12. method balance : Euro.c
  13. method deposit : Euro.c -> unit
  14. method withdraw : Euro.c -> Euro.c
  15. end
  1. let c = new account in c # deposit (euro 100.); c # withdraw (euro 50.);;
  2. - : Euro.c = <obj>

We now refine this definition with a method to compute interest.

  1. class account_with_interests =
  2. object (self)
  3. inherit account
  4. method private interest = self # deposit (self # balance # times 0.03)
  5. end;;
  6. class account_with_interests :
  7. object
  8. val mutable balance : Euro.c
  9. method balance : Euro.c
  10. method deposit : Euro.c -> unit
  11. method private interest : unit
  12. method withdraw : Euro.c -> Euro.c
  13. end

We make the method interest private, since clearly it should not becalled freely from the outside. Here, it is only made accessible to subclassesthat will manage monthly or yearly updates of the account.

We should soon fix a bug in the current definition: the deposit method canbe used for withdrawing money by depositing negative amounts. We canfix this directly:

  1. class safe_account =
  2. object
  3. inherit account
  4. method deposit x = if zero#leq x then balance <- balance#plus x
  5. end;;
  6. class safe_account :
  7. object
  8. val mutable balance : Euro.c
  9. method balance : Euro.c
  10. method deposit : Euro.c -> unit
  11. method withdraw : Euro.c -> Euro.c
  12. end

However, the bug might be fixed more safely by the following definition:

  1. class safe_account =
  2. object
  3. inherit account as unsafe
  4. method deposit x =
  5. if zero#leq x then unsafe # deposit x
  6. else raise (Invalid_argument "deposit")
  7. end;;
  8. class safe_account :
  9. object
  10. val mutable balance : Euro.c
  11. method balance : Euro.c
  12. method deposit : Euro.c -> unit
  13. method withdraw : Euro.c -> Euro.c
  14. end

In particular, this does not require the knowledge of the implementation ofthe method deposit.

To keep track of operations, we extend the class with a mutable fieldhistory and a private method trace to add an operation in thelog. Then each method to be traced is redefined.

  1. type 'a operation = Deposit of 'a | Retrieval of 'a;;
  2. type 'a operation = Deposit of 'a | Retrieval of 'a
  1. class account_with_history =
  2. object (self)
  3. inherit safe_account as super
  4. val mutable history = []
  5. method private trace x = history <- x :: history
  6. method deposit x = self#trace (Deposit x); super#deposit x
  7. method withdraw x = self#trace (Retrieval x); super#withdraw x
  8. method history = List.rev history
  9. end;;
  10. class account_with_history :
  11. object
  12. val mutable balance : Euro.c
  13. val mutable history : Euro.c operation list
  14. method balance : Euro.c
  15. method deposit : Euro.c -> unit
  16. method history : Euro.c operation list
  17. method private trace : Euro.c operation -> unit
  18. method withdraw : Euro.c -> Euro.c
  19. end

One may wish to open an account and simultaneously deposit some initialamount. Although the initial implementation did not address thisrequirement, it can be achieved by using an initializer.

  1. class account_with_deposit x =
  2. object
  3. inherit account_with_history
  4. initializer balance <- x
  5. end;;
  6. class account_with_deposit :
  7. Euro.c ->
  8. object
  9. val mutable balance : Euro.c
  10. val mutable history : Euro.c operation list
  11. method balance : Euro.c
  12. method deposit : Euro.c -> unit
  13. method history : Euro.c operation list
  14. method private trace : Euro.c operation -> unit
  15. method withdraw : Euro.c -> Euro.c
  16. end

A better alternative is:

  1. class account_with_deposit x =
  2. object (self)
  3. inherit account_with_history
  4. initializer self#deposit x
  5. end;;
  6. class account_with_deposit :
  7. Euro.c ->
  8. object
  9. val mutable balance : Euro.c
  10. val mutable history : Euro.c operation list
  11. method balance : Euro.c
  12. method deposit : Euro.c -> unit
  13. method history : Euro.c operation list
  14. method private trace : Euro.c operation -> unit
  15. method withdraw : Euro.c -> Euro.c
  16. end

Indeed, the latter is safer since the call to deposit will automaticallybenefit from safety checks and from the trace.Let’s test it:

  1. let ccp = new account_with_deposit (euro 100.) in
  2. let _balance = ccp#withdraw (euro 50.) in
  3. ccp#history;;
  4. - : Euro.c operation list = [Deposit <obj>; Retrieval <obj>]

Closing an account can be done with the following polymorphic function:

  1. let close c = c#withdraw c#balance;;
  2. val close : < balance : 'a; withdraw : 'a -> 'b; .. > -> 'b = <fun>

Of course, this applies to all sorts of accounts.

Finally, we gather several versions of the account into a module Accountabstracted over some currency.

  1. let today () = (01,01,2000) (* an approximation *)
  2. module Account (M:MONEY) =
  3. struct
  4. type m = M.c
  5. let m = new M.c
  6. let zero = m 0.
  7. class bank =
  8. object (self)
  9. val mutable balance = zero
  10. method balance = balance
  11. val mutable history = []
  12. method private trace x = history <- x::history
  13. method deposit x =
  14. self#trace (Deposit x);
  15. if zero#leq x then balance <- balance # plus x
  16. else raise (Invalid_argument "deposit")
  17. method withdraw x =
  18. if x#leq balance then
  19. (balance <- balance # plus (neg x); self#trace (Retrieval x); x)
  20. else zero
  21. method history = List.rev history
  22. end
  23. class type client_view =
  24. object
  25. method deposit : m -> unit
  26. method history : m operation list
  27. method withdraw : m -> m
  28. method balance : m
  29. end
  30. class virtual check_client x =
  31. let y = if (m 100.)#leq x then x
  32. else raise (Failure "Insufficient initial deposit") in
  33. object (self)
  34. initializer self#deposit y
  35. method virtual deposit: m -> unit
  36. end
  37. module Client (B : sig class bank : client_view end) =
  38. struct
  39. class account x : client_view =
  40. object
  41. inherit B.bank
  42. inherit check_client x
  43. end
  44. let discount x =
  45. let c = new account x in
  46. if today() < (1998,10,30) then c # deposit (m 100.); c
  47. end
  48. end;;
  49.  

This shows the use of modules to group several class definitions that can infact be thought of as a single unit. This unit would be provided by a bankfor both internal and external uses.This is implemented as a functor that abstracts over the currency so thatthe same code can be used to provide accounts in different currencies.

The class bank is the real implementation of the bank account (itcould have been inlined). This is the one that will be used for furtherextensions, refinements, etc. Conversely, the client will only be given the client view.

  1. module Euro_account = Account(Euro);;
  2.  
  1. module Client = Euro_account.Client (Euro_account);;
  2.  
  1. new Client.account (new Euro.c 100.);;
  2.  

Hence, the clients do not have direct access to the balance, nor thehistory of their own accounts. Their only way to change their balance isto deposit or withdraw money. It is important to give the clientsa class and not just the ability to create accounts (such as thepromotional discount account), so that they canpersonalize their account.For instance, a client may refine the deposit and withdraw methodsso as to do his own financial bookkeeping, automatically. On theother hand, the function discount is given as such, with nopossibility for further personalization.

It is important to provide the client’s view as a functorClient so that client accounts can still be built after a possiblespecialization of the bank.The functor Client may remain unchanged and be passedthe new definition to initialize a client’s view of the extended account.

  1. module Investment_account (M : MONEY) =
  2. struct
  3. type m = M.c
  4. module A = Account(M)
  5. class bank =
  6. object
  7. inherit A.bank as super
  8. method deposit x =
  9. if (new M.c 1000.)#leq x then
  10. print_string "Would you like to invest?";
  11. super#deposit x
  12. end
  13. module Client = A.Client
  14. end;;
  15.  

The functor Client may also be redefined when some new features of theaccount can be given to the client.

  1. module Internet_account (M : MONEY) =
  2. struct
  3. type m = M.c
  4. module A = Account(M)
  5. class bank =
  6. object
  7. inherit A.bank
  8. method mail s = print_string s
  9. end
  10. class type client_view =
  11. object
  12. method deposit : m -> unit
  13. method history : m operation list
  14. method withdraw : m -> m
  15. method balance : m
  16. method mail : string -> unit
  17. end
  18. module Client (B : sig class bank : client_view end) =
  19. struct
  20. class account x : client_view =
  21. object
  22. inherit B.bank
  23. inherit A.check_client x
  24. end
  25. end
  26. end;;
  27.  

6.2 Simple modules as classes

One may wonder whether it is possible to treat primitive types such asintegers and strings as objects. Although this is usually uninterestingfor integers or strings, there may be some situations wherethis is desirable. The class money above is such an example.We show here how to do it for strings.

6.2.1 Strings

A naive definition of strings as objects could be:

  1. class ostring s =
  2. object
  3. method get n = String.get s n
  4. method print = print_string s
  5. method escaped = new ostring (String.escaped s)
  6. end;;
  7. class ostring :
  8. string ->
  9. object
  10. method escaped : ostring
  11. method get : int -> char
  12. method print : unit
  13. end

However, the method escaped returns an object of the class ostring,and not an object of the current class. Hence, if the class is furtherextended, the method escaped will only return an object of the parentclass.

  1. class sub_string s =
  2. object
  3. inherit ostring s
  4. method sub start len = new sub_string (String.sub s start len)
  5. end;;
  6. class sub_string :
  7. string ->
  8. object
  9. method escaped : ostring
  10. method get : int -> char
  11. method print : unit
  12. method sub : int -> int -> sub_string
  13. end

As seen in section 3.16, the solution is to usefunctional update instead. We need to create an instance variablecontaining the representation s of the string.

  1. class better_string s =
  2. object
  3. val repr = s
  4. method get n = String.get repr n
  5. method print = print_string repr
  6. method escaped = {< repr = String.escaped repr >}
  7. method sub start len = {< repr = String.sub s start len >}
  8. end;;
  9. class better_string :
  10. string ->
  11. object ('a)
  12. val repr : string
  13. method escaped : 'a
  14. method get : int -> char
  15. method print : unit
  16. method sub : int -> int -> 'a
  17. end

As shown in the inferred type, the methods escaped and sub now returnobjects of the same type as the one of the class.

Another difficulty is the implementation of the method concat.In order to concatenate a string with another string of the same class,one must be able to access the instance variable externally. Thus, a methodrepr returning s must be defined. Here is the correct definition ofstrings:

  1. class ostring s =
  2. object (self : 'mytype)
  3. val repr = s
  4. method repr = repr
  5. method get n = String.get repr n
  6. method print = print_string repr
  7. method escaped = {< repr = String.escaped repr >}
  8. method sub start len = {< repr = String.sub s start len >}
  9. method concat (t : 'mytype) = {< repr = repr ^ t#repr >}
  10. end;;
  11. class ostring :
  12. string ->
  13. object ('a)
  14. val repr : string
  15. method concat : 'a -> 'a
  16. method escaped : 'a
  17. method get : int -> char
  18. method print : unit
  19. method repr : string
  20. method sub : int -> int -> 'a
  21. end

Another constructor of the class string can be defined to return a newstring of a given length:

  1. class cstring n = ostring (String.make n ' ');;
  2. class cstring : int -> ostring

Here, exposing the representation of strings is probably harmless. We docould also hide the representation of strings as we hid the currency in theclass money of section 3.17.

Stacks

There is sometimes an alternative between using modules or classes forparametric data types.Indeed, there are situations when the two approaches are quite similar.For instance, a stack can be straightforwardly implemented as a class:

  1. exception Empty;;
  2. exception Empty
  1. class ['a] stack =
  2. object
  3. val mutable l = ([] : 'a list)
  4. method push x = l <- x::l
  5. method pop = match l with [] -> raise Empty | a::l' -> l <- l'; a
  6. method clear = l <- []
  7. method length = List.length l
  8. end;;
  9. class ['a] stack :
  10. object
  11. val mutable l : 'a list
  12. method clear : unit
  13. method length : int
  14. method pop : 'a
  15. method push : 'a -> unit
  16. end

However, writing a method for iterating over a stack is moreproblematic. A method fold would have type('b -> 'a -> 'b) -> 'b -> 'b. Here 'a is the parameter of the stack.The parameter 'b is not related to the class 'a stack but to theargument that will be passed to the method fold.A naive approach is to make 'b an extra parameter of class stack:

  1. class ['a, 'b] stack2 =
  2. object
  3. inherit ['a] stack
  4. method fold f (x : 'b) = List.fold_left f x l
  5. end;;
  6. class ['a, 'b] stack2 :
  7. object
  8. val mutable l : 'a list
  9. method clear : unit
  10. method fold : ('b -> 'a -> 'b) -> 'b -> 'b
  11. method length : int
  12. method pop : 'a
  13. method push : 'a -> unit
  14. end

However, the method fold of a given object can only beapplied to functions that all have the same type:

  1. let s = new stack2;;
  2. val s : ('_weak1, '_weak2) stack2 = <obj>
  1. s#fold ( + ) 0;;
  2. - : int = 0
  1. s;;
  2. - : (int, int) stack2 = <obj>

A better solution is to use polymorphic methods, which wereintroduced in OCaml version 3.05. Polymorphic methods makesit possible to treat the type variable 'b in the type of fold asuniversally quantified, giving fold the polymorphic typeForall 'b. ('b -> 'a -> 'b) -> 'b -> 'b.An explicit type declaration on the method fold is required, sincethe type checker cannot infer the polymorphic type by itself.

  1. class ['a] stack3 =
  2. object
  3. inherit ['a] stack
  4. method fold : 'b. ('b -> 'a -> 'b) -> 'b -> 'b
  5. = fun f x -> List.fold_left f x l
  6. end;;
  7. class ['a] stack3 :
  8. object
  9. val mutable l : 'a list
  10. method clear : unit
  11. method fold : ('b -> 'a -> 'b) -> 'b -> 'b
  12. method length : int
  13. method pop : 'a
  14. method push : 'a -> unit
  15. end

6.2.2 Hashtbl

A simplified version of object-oriented hash tables should have thefollowing class type.

  1. class type ['a, 'b] hash_table =
  2. object
  3. method find : 'a -> 'b
  4. method add : 'a -> 'b -> unit
  5. end;;
  6. class type ['a, 'b] hash_table =
  7. object method add : 'a -> 'b -> unit method find : 'a -> 'b end

A simple implementation, which is quite reasonable for small hash tables isto use an association list:

  1. class ['a, 'b] small_hashtbl : ['a, 'b] hash_table =
  2. object
  3. val mutable table = []
  4. method find key = List.assoc key table
  5. method add key valeur = table <- (key, valeur) :: table
  6. end;;
  7. class ['a, 'b] small_hashtbl : ['a, 'b] hash_table

A better implementation, and one that scales up better, is to use atrue hash table… whose elements are small hash tables!

  1. class ['a, 'b] hashtbl size : ['a, 'b] hash_table =
  2. object (self)
  3. val table = Array.init size (fun i -> new small_hashtbl)
  4. method private hash key =
  5. (Hashtbl.hash key) mod (Array.length table)
  6. method find key = table.(self#hash key) # find key
  7. method add key = table.(self#hash key) # add key
  8. end;;
  9. class ['a, 'b] hashtbl : int -> ['a, 'b] hash_table

6.2.3 Sets

Implementing sets leads to another difficulty. Indeed, the methodunion needs to be able to access the internal representation ofanother object of the same class.

This is another instance of friend functions as seen in section3.17. Indeed, this is the same mechanism used in the moduleSet in the absence of objects.

In the object-oriented version of sets, we only need to add an additionalmethod tag to return the representation of a set. Since sets areparametric in the type of elements, the method tag has a parametric type'a tag, concrete withinthe module definition but abstract in its signature.From outside, it will then be guaranteed that two objects with a method tagof the same type will share the same representation.

  1. module type SET =
  2. sig
  3. type 'a tag
  4. class ['a] c :
  5. object ('b)
  6. method is_empty : bool
  7. method mem : 'a -> bool
  8. method add : 'a -> 'b
  9. method union : 'b -> 'b
  10. method iter : ('a -> unit) -> unit
  11. method tag : 'a tag
  12. end
  13. end;;
  14.  
  1. module Set : SET =
  2. struct
  3. let rec merge l1 l2 =
  4. match l1 with
  5. [] -> l2
  6. | h1 :: t1 ->
  7. match l2 with
  8. [] -> l1
  9. | h2 :: t2 ->
  10. if h1 < h2 then h1 :: merge t1 l2
  11. else if h1 > h2 then h2 :: merge l1 t2
  12. else merge t1 l2
  13. type 'a tag = 'a list
  14. class ['a] c =
  15. object (_ : 'b)
  16. val repr = ([] : 'a list)
  17. method is_empty = (repr = [])
  18. method mem x = List.exists (( = ) x) repr
  19. method add x = {< repr = merge [x] repr >}
  20. method union (s : 'b) = {< repr = merge repr s#tag >}
  21. method iter (f : 'a -> unit) = List.iter f repr
  22. method tag = repr
  23. end
  24. end;;
  25.  

6.3 The subject/observer pattern

The following example, known as the subject/observer pattern, is oftenpresented in the literature as a difficult inheritance problem withinter-connected classes.The general pattern amounts to the definition a pair of twoclasses that recursively interact with one another.

The class observer has a distinguished method notify that requirestwo arguments, a subject and an event to execute an action.

  1. class virtual ['subject, 'event] observer =
  2. object
  3. method virtual notify : 'subject -> 'event -> unit
  4. end;;
  5. class virtual ['subject, 'event] observer :
  6. object method virtual notify : 'subject -> 'event -> unit end

The class subject remembers a list of observers in an instance variable,and has a distinguished method notify_observers to broadcast the messagenotify to all observers with a particular event e.

  1. class ['observer, 'event] subject =
  2. object (self)
  3. val mutable observers = ([]:'observer list)
  4. method add_observer obs = observers <- (obs :: observers)
  5. method notify_observers (e : 'event) =
  6. List.iter (fun x -> x#notify self e) observers
  7. end;;
  8. class ['a, 'event] subject :
  9. object ('b)
  10. constraint 'a = < notify : 'b -> 'event -> unit; .. >
  11. val mutable observers : 'a list
  12. method add_observer : 'a -> unit
  13. method notify_observers : 'event -> unit
  14. end

The difficulty usually lies in defining instances of the pattern aboveby inheritance. This can be done in a natural and obvious manner inOCaml, as shown on the following example manipulating windows.

  1. type event = Raise | Resize | Move;;
  2. type event = Raise | Resize | Move
  1. let string_of_event = function
  2. Raise -> "Raise" | Resize -> "Resize" | Move -> "Move";;
  3. val string_of_event : event -> string = <fun>
  1. let count = ref 0;;
  2. val count : int ref = {contents = 0}
  1. class ['observer] window_subject =
  2. let id = count := succ !count; !count in
  3. object (self)
  4. inherit ['observer, event] subject
  5. val mutable position = 0
  6. method identity = id
  7. method move x = position <- position + x; self#notify_observers Move
  8. method draw = Printf.printf "{Position = %d}\n" position;
  9. end;;
  10. class ['a] window_subject :
  11. object ('b)
  12. constraint 'a = < notify : 'b -> event -> unit; .. >
  13. val mutable observers : 'a list
  14. val mutable position : int
  15. method add_observer : 'a -> unit
  16. method draw : unit
  17. method identity : int
  18. method move : int -> unit
  19. method notify_observers : event -> unit
  20. end
  1. class ['subject] window_observer =
  2. object
  3. inherit ['subject, event] observer
  4. method notify s e = s#draw
  5. end;;
  6. class ['a] window_observer :
  7. object
  8. constraint 'a = < draw : unit; .. >
  9. method notify : 'a -> event -> unit
  10. end

As can be expected, the type of window is recursive.

  1. let window = new window_subject;;
  2. val window : < notify : 'a -> event -> unit; _.. > window_subject as 'a =
  3. <obj>

However, the two classes of window_subject and window_observer are notmutually recursive.

  1. let window_observer = new window_observer;;
  2. val window_observer : < draw : unit; _.. > window_observer = <obj>
  1. window#add_observer window_observer;;
  2. - : unit = ()
  1. window#move 1;;
  2. {Position = 1}
  3. - : unit = ()

Classes window_observer and window_subject can still be extended byinheritance. For instance, one may enrich the subject with newbehaviors and refine the behavior of the observer.

  1. class ['observer] richer_window_subject =
  2. object (self)
  3. inherit ['observer] window_subject
  4. val mutable size = 1
  5. method resize x = size <- size + x; self#notify_observers Resize
  6. val mutable top = false
  7. method raise = top <- true; self#notify_observers Raise
  8. method draw = Printf.printf "{Position = %d; Size = %d}\n" position size;
  9. end;;
  10. class ['a] richer_window_subject :
  11. object ('b)
  12. constraint 'a = < notify : 'b -> event -> unit; .. >
  13. val mutable observers : 'a list
  14. val mutable position : int
  15. val mutable size : int
  16. val mutable top : bool
  17. method add_observer : 'a -> unit
  18. method draw : unit
  19. method identity : int
  20. method move : int -> unit
  21. method notify_observers : event -> unit
  22. method raise : unit
  23. method resize : int -> unit
  24. end
  1. class ['subject] richer_window_observer =
  2. object
  3. inherit ['subject] window_observer as super
  4. method notify s e = if e <> Raise then s#raise; super#notify s e
  5. end;;
  6. class ['a] richer_window_observer :
  7. object
  8. constraint 'a = < draw : unit; raise : unit; .. >
  9. method notify : 'a -> event -> unit
  10. end

We can also create a different kind of observer:

  1. class ['subject] trace_observer =
  2. object
  3. inherit ['subject, event] observer
  4. method notify s e =
  5. Printf.printf
  6. "<Window %d <== %s>\n" s#identity (string_of_event e)
  7. end;;
  8. class ['a] trace_observer :
  9. object
  10. constraint 'a = < identity : int; .. >
  11. method notify : 'a -> event -> unit
  12. end

and attach several observers to the same object:

  1. let window = new richer_window_subject;;
  2. val window :
  3. < notify : 'a -> event -> unit; _.. > richer_window_subject as 'a = <obj>
  1. window#add_observer (new richer_window_observer);;
  2. - : unit = ()
  1. window#add_observer (new trace_observer);;
  2. - : unit = ()
  1. window#move 1; window#resize 2;;
  2. <Window 1 <== Move>
  3. <Window 1 <== Raise>
  4. {Position = 1; Size = 1}
  5. {Position = 1; Size = 1}
  6. <Window 1 <== Resize>
  7. <Window 1 <== Raise>
  8. {Position = 1; Size = 3}
  9. {Position = 1; Size = 3}
  10. - : unit = ()