1 ObjectOrientedProgramming
2 =========================
4 <:StandardML:Standard ML> does not have explicit support for
5 object-oriented programming. Here are some papers that show how to
6 express certain object-oriented concepts in SML.
8 * <!Cite(Berthomieu00, OO Programming styles in ML)>
10 * <!Cite(ThorupTofte94, Object-oriented programming and Standard ML)>
12 * <!Cite(LarsenNiss04, mGTK: An SML binding of Gtk+)>
14 * <!Cite(FluetPucella06, Phantom Types and Subtyping)>
16 The question of OO programming in SML comes up every now and then.
17 The following discusses a simple object-oriented (OO) programming
18 technique in Standard ML. The reader is assumed to be able to read
24 SML doesn't provide subtyping, but it does provide parametric
25 polymorphism, which can be used to encode some forms of subtyping.
26 Most articles on OO programming in SML concentrate on such encoding
27 techniques. While those techniques are interesting -- and it is
28 recommended to read such articles -- and sometimes useful, it seems
29 that basically all OO gurus agree that (deep) subtyping (or
30 inheritance) hierarchies aren't as practical as they were thought to
31 be in the early OO days. "Good", flexible, "OO" designs tend to have
38 - - -+-------+-------+- - -
44 and deep inheritance hierarchies
58 tend to be signs of design mistakes. There are good underlying
59 reasons for this, but a thorough discussion is not in the scope of
60 this article. However, the point is that perhaps the encoding of
61 subtyping is not as important as one might believe. In the following
62 we ignore subtyping and rather concentrate on a very simple and basic
63 dynamic dispatch technique.
66 == Dynamic Dispatch Using a Recursive Record of Functions ==
68 Quite simply, the basic idea is to implement a "virtual function
69 table" using a record that is wrapped inside a (possibly recursive)
70 datatype. Let's first take a look at a simple concrete example.
72 Consider the following Java interface:
75 public interface Counter {
81 We can translate the `Counter` interface to SML as follows:
85 datatype counter = Counter of {inc : unit -> unit, get : unit -> int}
88 Each value of type `counter` can be thought of as an object that
89 responds to two messages `inc` and `get`. To actually send messages
90 to a counter, it is useful to define auxiliary functions
95 fun mk m (Counter t) = m t ()
102 that basically extract the "function table" `t` from a counter object
103 and then select the specified method `m` from the table.
105 Let's then implement a simple function that increments a counter until a
106 given maximum is reached:
110 fun incUpto counter max = while cGet counter < max do cInc counter
113 You can easily verify that the above code compiles even without any
114 concrete implementation of a counter, thus it is clear that it doesn't
115 depend on a particular counter implementation.
117 Let's then implement a couple of counters. First consider the
118 following Java class implementing the `Counter` interface given earlier.
121 public class BasicCounter implements Counter {
123 public BasicCounter(int initialCnt) { this.cnt = initialCnt; }
124 public void inc() { this.cnt += 1; }
125 public int get() { return this.cnt; }
129 We can translate the above to SML as follows:
133 fun newBasicCounter initialCnt = let
134 val cnt = ref initialCnt
136 Counter {inc = fn () => cnt := !cnt + 1,
141 The SML function `newBasicCounter` can be described as a constructor
142 function for counter objects of the `BasicCounter` "class". We can
143 also have other counter implementations. Here is the constructor for
144 a counter decorator that logs messages:
148 fun newLoggedCounter counter =
149 Counter {inc = fn () => (print "inc\n" ; cInc counter),
150 get = fn () => (print "get\n" ; cGet counter)}
153 The `incUpto` function works just as well with objects of either
158 val aCounter = newBasicCounter 0
159 val () = incUpto aCounter 5
160 val () = print (Int.toString (cGet aCounter) ^"\n")
162 val aCounter = newLoggedCounter (newBasicCounter 0)
163 val () = incUpto aCounter 5
164 val () = print (Int.toString (cGet aCounter) ^"\n")
167 In general, a dynamic dispatch interface is represented as a record
168 type wrapped inside a datatype. Each field of the record corresponds
169 to a public method or field of the object:
174 Interface of {method : t1 -> t2,
176 mutableField : t ref}
179 The reason for wrapping the record inside a datatype is that records,
180 in SML, can not be recursive. However, SML datatypes can be
181 recursive. A record wrapped in a datatype can contain fields that
182 contain the datatype. For example, an interface such as `Cloneable`
186 datatype cloneable = Cloneable of {clone : unit -> cloneable}
189 can be represented using recursive datatypes.
191 Like in OO languages, interfaces are abstract and can not be
192 instantiated to produce objects. To be able to instantiate objects,
193 the constructors of a concrete class are needed. In SML, we can
194 implement constructors as simple functions from arbitrary arguments to
195 values of the interface type. Such a constructor function can
196 encapsulate arbitrary private state and functions using lexical
197 closure. It is also easy to share implementations of methods between
198 two or more constructors.
200 While the `Counter` example is rather trivial, it should not be
201 difficult to see that this technique quite simply doesn't require a huge
202 amount of extra verbiage and is more than usable in practice.
205 == SML Modules and Dynamic Dispatch ==
207 One might wonder about how SML modules and the dynamic dispatch
208 technique work together. Let's investigate! Let's use a simple
209 dispenser framework as a concrete example. (Note that this isn't
210 intended to be an introduction to the SML module system.)
212 === Programming with SML Modules ===
214 Using SML signatures we can specify abstract data types (ADTs) such as
215 dispensers. Here is a signature for an "abstract" functional (as
216 opposed to imperative) dispenser:
220 signature ABSTRACT_DISPENSER = sig
222 val isEmpty : 'a t -> bool
223 val push : 'a * 'a t -> 'a t
224 val pop : 'a t -> ('a * 'a t) option
228 The term "abstract" in the name of the signature refers to the fact that
229 the signature gives no way to instantiate a dispenser. It has nothing to
230 do with the concept of abstract data types.
232 Using SML functors we can write "generic" algorithms that manipulate
233 dispensers of an unknown type. Here are a couple of very simple
238 functor DispenserAlgs (D : ABSTRACT_DISPENSER) = struct
241 fun pushAll (xs, d) = foldl push d xs
244 fun lp (xs, NONE) = rev xs
245 | lp (xs, SOME (x, d)) = lp (x::xs, pop d)
250 fun cp (from, to) = pushAll (popAll from, to)
254 As one can easily verify, the above compiles even without any concrete
255 dispenser structure. Functors essentially provide a form a static
256 dispatch that one can use to break compile-time dependencies.
258 We can also give a signature for a concrete dispenser
262 signature DISPENSER = sig
263 include ABSTRACT_DISPENSER
268 and write any number of concrete structures implementing the signature.
269 For example, we could implement stacks
273 structure Stack :> DISPENSER = struct
278 val pop = List.getItem
286 structure Queue :> DISPENSER = struct
287 datatype 'a t = T of 'a list * 'a list
288 val empty = T ([], [])
289 val isEmpty = fn T ([], _) => true | _ => false
290 val normalize = fn ([], ys) => (rev ys, []) | q => q
291 fun push (y, T (xs, ys)) = T (normalize (xs, y::ys))
292 val pop = fn (T (x::xs, ys)) => SOME (x, T (normalize (xs, ys))) | _ => NONE
296 One can now write code that uses either the `Stack` or the `Queue`
297 dispenser. One can also instantiate the previously defined functor to
298 create functions for manipulating dispensers of a type:
302 structure S = DispenserAlgs (Stack)
303 val [4,3,2,1] = S.popAll (S.pushAll ([1,2,3,4], Stack.empty))
305 structure Q = DispenserAlgs (Queue)
306 val [1,2,3,4] = Q.popAll (Q.pushAll ([1,2,3,4], Queue.empty))
309 There is no dynamic dispatch involved at the module level in SML. An
310 attempt to do dynamic dispatch
314 val q = Q.push (1, Stack.empty)
317 will give a type error.
319 === Combining SML Modules and Dynamic Dispatch ===
321 Let's then combine SML modules and the dynamic dispatch technique
322 introduced in this article. First we define an interface for
327 structure Dispenser = struct
329 I of {isEmpty : unit -> bool,
331 pop : unit -> ('a * 'a t) option}
335 fun isEmpty t = O#isEmpty t ()
336 fun push (v, t) = O#push t v
337 fun pop t = O#pop t ()
341 The `Dispenser` module, which we can think of as an interface for
342 dispensers, implements the `ABSTRACT_DISPENSER` signature using
343 the dynamic dispatch technique, but we leave the signature ascription
346 Then we define a `DispenserClass` functor that makes a "class" out of
347 a given dispenser module:
351 functor DispenserClass (D : DISPENSER) : DISPENSER = struct
355 I {isEmpty = fn () => D.isEmpty d,
356 push = fn x => make (D.push (x, d)),
360 | SOME (x, d) => SOME (x, make d)}
363 I {isEmpty = fn () => true,
364 push = fn x => make (D.push (x, D.empty)),
369 Finally we seal the `Dispenser` module:
373 structure Dispenser : ABSTRACT_DISPENSER = Dispenser
376 This isn't necessary for type safety, because the unsealed `Dispenser`
377 module does not allow one to break encapsulation, but makes sure that
378 only the `DispenserClass` functor can create dispenser classes
379 (because the constructor `Dispenser.I` is no longer accessible).
381 Using the `DispenserClass` functor we can turn any concrete dispenser
382 module into a dispenser class:
386 structure StackClass = DispenserClass (Stack)
387 structure QueueClass = DispenserClass (Queue)
390 Each dispenser class implements the same dynamic dispatch interface
391 and the `ABSTRACT_DISPENSER` -signature.
393 Because the dynamic dispatch `Dispenser` module implements the
394 `ABSTRACT_DISPENSER`-signature, we can use it to instantiate the
395 `DispenserAlgs`-functor:
399 structure D = DispenserAlgs (Dispenser)
402 The resulting `D` module, like the `Dispenser` module, works with
403 any dispenser class and uses dynamic dispatch:
407 val [4, 3, 2, 1] = D.popAll (D.pushAll ([1, 2, 3, 4], StackClass.empty))
408 val [1, 2, 3, 4] = D.popAll (D.pushAll ([1, 2, 3, 4], QueueClass.empty))