Vicente Wagner
Vicente Wagner
Vicente Wagner
Antonio Vicente
[email protected]
Earl Wagner
[email protected]
Abstract
The GOF Design Patterns book is an important piece of any professional programmer’s library. These
patterns are generally considered to be an indication of good design and development practices. By
giving an implementation of these patterns in OCaml we expected to better understand the importance
of OCaml’s advanced language features and provide other developers with an implementation of these
familiar concepts in order to reduce the effort required to learn this language. As in the case of Smalltalk
and Scheme+GLOS, OCaml’s higher order features allows for simple elegant implementation of some of
the patterns while others were much harder due to the OCaml’s restrictive type system.
1
Contents
1 Background and Motivation 3
4 Creational Patterns 5
4.1 Abstract Factory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
4.2 Builder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4.3 Factory Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4.4 Prototype . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
4.5 Singleton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5 Structural Patterns 8
5.1 Adapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5.2 Bridge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5.3 Composite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5.4 Decorator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
5.5 Facade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
5.6 Flyweight . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
5.7 Proxy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
6 Behavior Patterns 11
6.1 Chain of Responsibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
6.2 Command . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
6.3 Interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
6.4 Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
6.5 Mediator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
6.6 Memento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
6.7 Observer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
6.8 State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
6.9 Strategy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
6.10 Template Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
6.11 Visitor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
7 References 18
2
1 Background and Motivation
Throughout this course we have seen many examples of methodologies and tools that can be used to
reduce the burden of working in a software project. These tools focus force the programmer to take a
different view toward the software development process and in exchange provide the programmer with
ways to write code that is easier to check for correctness.
OCaml helps developers by forcing them to write software components that are well typed and more
modular. It has a number of interesting features like first-class functions, pattern matching, lazy evalua-
tion and static type checking that, combined with a powerful module system, simplify the implementation
of many common programming tasks and help promote healthy coding practices. These features come at
the expense of expressiveness and complexity which is often enough to prevent developers from learning
and using OCaml in their development projects.
By trying to implement Gang of Four’s (GOF) Design Patterns in OCaml, we expected to reduce the
difficulties that new developers go though while trying to perform some common tasks in this language.
The Design Patterns book is a good start because of the large number of professional programmers
that know about these patterns and use them to make informed decision about how to design and
implement a software system. In the GOF’s book, the authors discuss how Smalltalk’s higher order
features simplify several of their patterns. This work was extended by several others including Greg
Sullivan who analyzed how the dynamic features of his GLOS extension to Scheme affect the expression
of many of these patterns.
Also, we noticed that very few programmers take full advantage of the OCaml’s module system.
Browsing though the code for Unison, we noticed that they make minimal use of functors and parame-
terized classes. We saw similar tendencies when looking at OCaml code written for ICFP programming
competitions. We hoped that our work motivates other developers to learn how to use these advanced
features and take advantage of them in their day to day programming tasks, or at least help us understand
why these features are not as widely used as they should.
3
language does not support up-casting or recursive type/module definitions. We can get around some of
these limitations by using our use of classes to those cases where they are strictly necessary and try to
be consistent with respect to what interface is used to access instances.
4
4 Creational Patterns
4.1 Abstract Factory
In ML, modules and classes are not first class objects. It is possible to get around this limitation by
using functors, but that can force us to have a large number of functors at the top level or modules that
need to be functorized just because some module they use is. Abstract factories can be used to group
together related modules and thus eliminate or significantly reduce the need many functorized top-level
modules. Functors can be used to reduce bindings inside the factory but much less effectively than using
reflection mechanisms.
Our implementation takes advantage of functors to define a family of abstract factories that satisfy the
MAZEFACTORY module type and create two instances that provide different kinds of doors. When we
are dealing with structures, the MAZEFACTORY module needs to expose basic functionality of doors,
rooms and mazes though its interface in order to allow the user to call methods on the abstract types
returned by the factory.
If we had used OCaml’s OO extensions, this pattern would look much more similar to the C++
implementation. We can now select to export an interface that is satisfied by the objects that the factory
returns in order to provide ways to interact with those objects.
absmazefactory.ml
open Mazefactory;;
open Enchanteddoor;;
open Normaldoor;;
open Enchantedroom;;
open Maze;;
open Doori;;
open Room;;
open Mazei;;
module Abstractmazefactory =
functor (DoorT:DOORI) ->
functor (RoomT:ROOM) ->
functor (MazeT:MAZEI) ->
struct
module CRoom = RoomT (DoorT)
module CMaze = MazeT (RoomT) (DoorT)
5
4.2 Builder
The builder pattern pretty much the same as in C++. You can pass the arguments to the builder either
by using functors, using pattern matching or regular objects. The builder pattern as a construction
protocol seems to be a universal pattern as Greg Sullivan points out, which seems to be among the list
of universal patterns.
maze.ml
open Doori;;
open Room;;
open Mazei;;
factorymethod.ml
exception Unknowndoor;;
module Normaldoor =
struct
type t = { id:int }
let buildDoor () = { id = 0 }
let doorIsOpen n = true
end;;
module Enchanteddoor =
struct
type t = { id:int; isOpen:bool; spell:string }
let buildDoor () = { id = 0; isOpen = false; spell = "currying" }
6
let doorIsOpen n = n.isOpen
end;;
factorymethod2.ml
class virtual door =
object (self : ’t)
method virtual doorOpen : unit -> unit
method virtual doorClose : unit -> unit
method virtual isOpen : unit -> bool
end;;
class normaldoor () =
object (self)
inherit door
val mutable opened = false
method doorOpen () = opened <- true
method doorClose () = opened <- false
method isOpen () = opened
end
class enchanteddoor () =
object (self)
inherit door
val mutable opened = false
val mutable cursed = true
method doorOpen () = if not cursed then opened <- true
method doorClose () = if not cursed then opened <- false
method isOpen () = opened
end
exception Unknowndoor
4.4 Prototype
There is no notion of reflection or a standard copy interface. It is up to the user to define and implement
such an interface if he wants to be able to copy objects. OCaml provides a way to perform functional
object updates which can simplify the implementation of the prototype pattern.
When defining or implementing a copy interface we will have to take into account the fact that OCaml
does not support casting objects up the class hierarchies. This problem can be solved by using the same
mechanisms used to implement the factory method. This suggests that OCaml would require at least
two “standard” copy interfaces and it is not clear that even then that would be enough.
7
4.5 Singleton
This pattern appears as a central concept in ML’s module system. Modules act as global singletons
that can provide all kinds of functionality. Functor instantiation is another good example how singletons
appear in ML.
5 Structural Patterns
5.1 Adapter
The GOF discuss class and object versions of Adapter. In the class version of the pattern, a subclass
of the Adaptee class is created and extra code is added to satisfy the Target interface. In the object
version, a class satisfies the Target by creating an instance of the Adaptee class and delegating to it.
As in the case for Smalltalk, it is possible to implement the Adapter pattern using anonymous
functions in Caml. The GOF point out that the disadvantage of using an object Adapter is it is more
difficult to change the Adaptee object’s behavior. This can be solved by making the Adapter class a
parameterized module with the Adaptee’s object specified at compile time. This is less flexible than
specifying the type of the Adaptee object at runtime, using an Abstract Factory, however it permits
static analysis.
adapter.ml
(*class version*)
class textshape s =
object
inherit windowwidget
method bbox () = (0,0), (8 * String.length s, 10)
end
(*instance version*)
exception Opnotsupported
let s = "test"
let wwa = new windowwidgetadaptor (fun () -> (0,0), (8 * String.length s, 10))
(fun x y -> raise Opnotsupported)
5.2 Bridge
As Greg Sullivan notes, the Bridge pattern, in which an implementation is defined separately from its
interface, appears to be a universal idea for abstraction.
5.3 Composite
In the composite pattern, the interface to a tree data structure is simplified by providing the same
interface for leaf and non-leaf nodes in the tree.
8
One question in implementing the Composite pattern is which methods for the Leaf class to incorpo-
rate into the Component interface. They give the example of calling ”draw” on a canvas, which then calls
”draw” on all of its children. This can be solved using closures with functions like ”map” and ”filter”,
which could apply the function to all of the Leaf objects. In this case, the canvas would receive ”map
draw” and call this on all of its children. The Leaf objects would then draw themselves.
5.4 Decorator
The Decorator pattern is used to dynamically add and remove responsibilities to and from objects. We
implement this as it is implemented in C++:
decorator.ml
class virtual visual_component =
object (self)
method virtual draw: unit -> unit
method virtual resize: unit -> unit
end;;
class basic_visual_component =
object (self)
inherit visual_component
method draw () = Printf.printf "draw\n"
method resize () = Printf.printf "resize\n"
end;;
(*
let bvc = new basic_visual_component;;
# let bvc = new basic_visual_component;;
val bvc : basic_visual_component = <obj>
# let bd = new border_decorator (bvc, 1);;
val bd : border_decorator = <obj>
# bd#draw;;
- : unit -> unit = <fun>
# bd#draw ();;
draw
draw_border
- : unit = ()
*)
9
5.5 Facade
The Facade pattern is an example of how to break down a program to increase modularity. ML’s module
system provides support this pattern directly by allowing the user can explicitly say exactly what they
want to export to specific clients.
5.6 Flyweight
These are basically the idea of making everything an object to abstract away representation of even the
simplest objects. this can be done by instantiating objects on module loading and then making a factory
method to access these new objects. This pattern is worth using for the same purposes as in C++.
5.7 Proxy
A Proxy is used to control access to another object. This is useful in the case that the object is located
over the network, as in the case of a network proxy, when the object is expensive to create and is
instantiated on demand, as in the case of a virtual proxy, when there should be different access rights
to the object, or when additional behaviors are required of a reference to the object, as in the case of a
smart reference.
There appears to be no easy way to implement a virtual proxy, nor smart reference in ML, and the
implementation network proxy depends upon the details of the network interface. However, a protection
proxy could be implemented using different signatures for the same structure. An example of this is
given in the “Extended Example: Managing Bank Accounts” section of the OCaml book. book. [2]
proxy.ml
# 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
10
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
6 Behavior Patterns
6.1 Chain of Responsibility
The implementation of this pattern in OCaml looks very similar to the C++ implementation. New
handlers can be added by overriding the handleRequest method and delegating any unhandled messages
to the next handler in the chain.
chain.ml
exception UnhandledRequest
11
| x::xs -> (x#handleRequest r:unit)
end
class helphandler =
object (self : ’a)
inherit [request]chain
method handleRequest h = print_string "handler 1"; match h with
(Help "test") -> print_string "request handled"; ()
| _ -> self#delegateRequest h
end
class helphandler2 =
object (self : ’a)
inherit [request]chain
method handleRequest r = print_string "handler 2"; match r with
(Help "test2") -> print_string "request 2 handled"
| _ -> self#delegateRequest r
end
class client () =
object(self)
val handleChain = let h1 = new helphandler in
let h2 = new helphandler2 in
h2#setNextHandler h1; h2
method dostuff () = handleChain#handleRequest (Help "test")
end
let main () =
let c = new client () in
c#dostuff();
print_newline();
exit(0);;
main ();;
6.2 Command
The Command pattern represents a call on an object as an object itself. This can be helpful when
information about the call is known, but the object that will receive the call is unknown. In essence,
this is currying, which ML supports directly. It may be that the call is required to be undo-able. In this
case, a more sophisticated solution will be necessary.
Here is the structure for the simple case:
command.ml
class [’a] receiver =
object (self)
method perform_action (action: (’a -> ’b)) = action self
end;;
12
6.3 Interpreter
look at desk calculator example. pattern matching makes interpreters easy to write. ocalm people seem
to jis over this
appears in web page as calculator example. pattern matching makes writting parsers and interpreters
easy.
An example of this pattern is given in the “Desktop Calculator Example” section of the OCaml book.
[2]
6.4 Iterator
The GOF distinguish between external iteration, when iteration is controlled by the client interacting
with the iterator, from internal iteration in which the client passes a function to the iterator for it to
perform on each element. Internal iteration is implemented by passing a curried function to List.map,
which then applies the function to each element of the list. External iteration can also be performed
using the List module, by applying the function to successive elements in a list.
6.5 Mediator
In this pattern, a Mediator coordinates the actions of a set of Colleagues. When one Colleague changes
and needs to make changes to other Colleagues, it notifies the Mediator, which itself interacts with
the other Colleagues. This requires that each Colleague has a reference to its Mediator, and that the
Mediator itself keeps track of all of the Colleagues. This cannot be implemented using ML modules,
since they are not permitted to be recursively dependent upon each other.
This pattern can be implemented using OCaml’s classes, in which case it is not much different from
the C++ case:
mediator.ml
class virtual [’widget] dialog_director =
object
method virtual widget_changed : ’widget -> unit
method virtual show_dialog : unit -> unit
end;;
6.6 Memento
While designing and implementing a marshalling protocol for ML, we would expect to run into many
of the same difficulties and design decisions that we saw while analysing the prototype pattern. The
main problem here is with the return type of a function for unmarshalling. You cannot up-cast the
unmarshalled value to return it to its original type.
As in the case of the prototype pattern, implementing the Memento pattern in ML poses many
difficulties.
6.7 Observer
This pattern requires mutually-recursive dependence among the subject and observer classes; the observer
must be aware of the type of the subjects in order to notify them and the subjects must be aware of the
type of the observer to notify it. This can be implemented with parameterized classes, but a meaningless
method call must be inserted into the code because OCaml’s static checker cannot infer the type of the
add observer unless you call that it somewhere in your code.
13
The following example of this pattern appears in the “Advanced Examples with Classes and Modules”
section of the OCaml documentation [1]
observer.ml
class virtual [’subject, ’event] observer =
object
method virtual notify : ’subject -> ’event -> unit
end;;
6.8 State
As with many of the patterns above, there are two ways to implement this pattern: using pattern matching
or objects. Pattern matching requires knowledge of the types of connection a priori and creating a new
type that can be used to store extra information. This kind of state objects are lightweight and allow for
centralized implementation of behavior suggested by the pattern. The other alternative is to define a class
that all the state classes derive from. These classes would have to be able to call each other’s constructors
and thus need to be defined in the same class block which is extremely ugly and inconvenient.
state.ml
type TCPConn = OpenConn of int | ClosedConn of string;;
14
let transmit connection data = match connection with
(OpenConn c) -> sendPacket connection data
| _ -> Error "closed connection";;
6.9 Strategy
The Strategy pattern is used to factor out algorithms from the classes that use them. The GOF put
the implementation of the algorithm in a separate class, but as Greg Sullivan points out, with first-class
functions, algorithms can be expressed as closures and passed into client classes directly.
Here is a context class that uses this approach:
strategy.ml
class [’a, ’b] context (strategy_init) =
object (self)
val my_strategy = (strategy_init: (’a -> ’b))
method operation x = my_strategy x
end;;
templatemethod.ml
class virtual abstract_template =
object (self)
method virtual hook_operation : unit -> unit
method operation () = self#hook_operation ()
end;;
class concrete_template =
object
inherit abstract_template as super
method operation () = super#operation
method hook_operation () = Printf.printf "operation\n"
end;;
6.11 Visitor
The visitor pattern expresses the need for full information about an object’s class that is needed when
trying to perform external operations on a group of objects. The inability to test class membership
dynamically or up cast values forces us to use pattern matching or double dispatch to implement this
pattern.
The pattern implementation provides full information about the node real type so it can perform
meaningful computations as the visitor now has full knowledge about the node’s type and methods. As
long as none of the classes are subclasses of each other, pattern matching protects the code from problems
that appear in other OO languages due to carelessness.
The other alternative is to define mutually-dependent classes similar to the ones used to implement
the observer pattern. This implementation suffers from the same typing problems that the observer
pattern suffers.
visitor-simple.ml
15
end
class floppy () =
object (self : ’a)
inherit equipment
method name () = "floppy"
method price1 () = 5
method price () = 5
end
class chassis () =
object (self)
inherit equipment
method name () = "chassis"
method price2 () = 10
method price () = 10
end
class pricevisitor () =
object
inherit equipmentvisitor
val mutable totalprice = 0
method visitFloppy f = totalprice <- totalprice + f#price ()
method visitChassis c = totalprice <- totalprice + c#price ()
end
visitor.ml
(*the equipment*)
class virtual [’equipmentvisitor]equipment =
object
method virtual name : unit -> string
method virtual price : unit -> int
method virtual accept : ’equipmentvisitor -> unit
end
16
class [’equipmentvisitor]floppy () =
object (self : ’a)
inherit [’equipmentvisitor]equipment
method name () = "floppy"
method price () = 5
method accept v = v#visitFloppy self
end
class [’equipmentvisitor]chassis () =
object (self)
inherit [’equipmentvisitor]equipment
method name () = "chassis"
method price () = 10
method accept v = v#visitChassis self
end
class [’equipmentvisitor]computer () =
object (self:’a)
inherit [’equipmentvisitor]compositeequipment
val mutable subcomponents = ([] : ’a list)
method name () = "chassis"
method price () = 10
method accept v = List.map (fun c -> c#accept v) subcomponents; ()
end
(*visitor classes*)
class virtual [’floppy, ’chassis] equipmentvisitor =
object (self)
method virtual visitFloppy : ’floppy -> unit
method virtual visitChassis : ’chassis -> unit
end
(*these 8 lines are essential, they make the visitor and the visitee
cross reference types*)
17
let _ = a#accept c;;
let _ = b#accept c;;
let _ = a#accept d;;
let _ = b#accept d;;
7 References
1. X. Leroy, D. Doligez, J. Garrigue, D. R’emy, and J. Vouillon. The Objective CAML system, documen-
tation and user’s manual, 2000. http://caml.inria.fr/ocaml/htmlman/.
3. G. Sullivan. Advanced Programming Language Features for Executable Design Patterns “Better Pat-
terns Through Reflection”, March 2002. MIT AI Lab, AI Memo 2002-005
18