Import Upstream version 20180207
[hcoop/debian/mlton.git] / doc / guide / src / ObjectOrientedProgramming.adoc
CommitLineData
7f918cf1
CE
1ObjectOrientedProgramming
2=========================
3
4<:StandardML:Standard ML> does not have explicit support for
5object-oriented programming. Here are some papers that show how to
6express certain object-oriented concepts in SML.
7
8* <!Cite(Berthomieu00, OO Programming styles in ML)>
9
10* <!Cite(ThorupTofte94, Object-oriented programming and Standard ML)>
11
12* <!Cite(LarsenNiss04, mGTK: An SML binding of Gtk+)>
13
14* <!Cite(FluetPucella06, Phantom Types and Subtyping)>
15
16The question of OO programming in SML comes up every now and then.
17The following discusses a simple object-oriented (OO) programming
18technique in Standard ML. The reader is assumed to be able to read
19Java and SML code.
20
21
22== Motivation ==
23
24SML doesn't provide subtyping, but it does provide parametric
25polymorphism, which can be used to encode some forms of subtyping.
26Most articles on OO programming in SML concentrate on such encoding
27techniques. While those techniques are interesting -- and it is
28recommended to read such articles -- and sometimes useful, it seems
29that basically all OO gurus agree that (deep) subtyping (or
30inheritance) hierarchies aren't as practical as they were thought to
31be in the early OO days. "Good", flexible, "OO" designs tend to have
32a flat structure
33
34----
35 Interface
36 ^
37 |
38- - -+-------+-------+- - -
39 | | |
40 ImplA ImplB ImplC
41----
42
43
44and deep inheritance hierarchies
45
46----
47ClassA
48 ^
49 |
50ClassB
51 ^
52 |
53ClassC
54 ^
55 |
56----
57
58tend to be signs of design mistakes. There are good underlying
59reasons for this, but a thorough discussion is not in the scope of
60this article. However, the point is that perhaps the encoding of
61subtyping is not as important as one might believe. In the following
62we ignore subtyping and rather concentrate on a very simple and basic
63dynamic dispatch technique.
64
65
66== Dynamic Dispatch Using a Recursive Record of Functions ==
67
68Quite simply, the basic idea is to implement a "virtual function
69table" using a record that is wrapped inside a (possibly recursive)
70datatype. Let's first take a look at a simple concrete example.
71
72Consider the following Java interface:
73
74----
75public interface Counter {
76 public void inc();
77 public int get();
78}
79----
80
81We can translate the `Counter` interface to SML as follows:
82
83[source,sml]
84----
85datatype counter = Counter of {inc : unit -> unit, get : unit -> int}
86----
87
88Each value of type `counter` can be thought of as an object that
89responds to two messages `inc` and `get`. To actually send messages
90to a counter, it is useful to define auxiliary functions
91
92[source,sml]
93----
94local
95 fun mk m (Counter t) = m t ()
96in
97 val cGet = mk#get
98 val cInc = mk#inc
99end
100----
101
102that basically extract the "function table" `t` from a counter object
103and then select the specified method `m` from the table.
104
105Let's then implement a simple function that increments a counter until a
106given maximum is reached:
107
108[source,sml]
109----
110fun incUpto counter max = while cGet counter < max do cInc counter
111----
112
113You can easily verify that the above code compiles even without any
114concrete implementation of a counter, thus it is clear that it doesn't
115depend on a particular counter implementation.
116
117Let's then implement a couple of counters. First consider the
118following Java class implementing the `Counter` interface given earlier.
119
120----
121public class BasicCounter implements Counter {
122 private int cnt;
123 public BasicCounter(int initialCnt) { this.cnt = initialCnt; }
124 public void inc() { this.cnt += 1; }
125 public int get() { return this.cnt; }
126}
127----
128
129We can translate the above to SML as follows:
130
131[source,sml]
132----
133fun newBasicCounter initialCnt = let
134 val cnt = ref initialCnt
135 in
136 Counter {inc = fn () => cnt := !cnt + 1,
137 get = fn () => !cnt}
138 end
139----
140
141The SML function `newBasicCounter` can be described as a constructor
142function for counter objects of the `BasicCounter` "class". We can
143also have other counter implementations. Here is the constructor for
144a counter decorator that logs messages:
145
146[source,sml]
147----
148fun newLoggedCounter counter =
149 Counter {inc = fn () => (print "inc\n" ; cInc counter),
150 get = fn () => (print "get\n" ; cGet counter)}
151----
152
153The `incUpto` function works just as well with objects of either
154class:
155
156[source,sml]
157----
158val aCounter = newBasicCounter 0
159val () = incUpto aCounter 5
160val () = print (Int.toString (cGet aCounter) ^"\n")
161
162val aCounter = newLoggedCounter (newBasicCounter 0)
163val () = incUpto aCounter 5
164val () = print (Int.toString (cGet aCounter) ^"\n")
165----
166
167In general, a dynamic dispatch interface is represented as a record
168type wrapped inside a datatype. Each field of the record corresponds
169to a public method or field of the object:
170
171[source,sml]
172----
173datatype interface =
174 Interface of {method : t1 -> t2,
175 immutableField : t,
176 mutableField : t ref}
177----
178
179The reason for wrapping the record inside a datatype is that records,
180in SML, can not be recursive. However, SML datatypes can be
181recursive. A record wrapped in a datatype can contain fields that
182contain the datatype. For example, an interface such as `Cloneable`
183
184[source,sml]
185----
186datatype cloneable = Cloneable of {clone : unit -> cloneable}
187----
188
189can be represented using recursive datatypes.
190
191Like in OO languages, interfaces are abstract and can not be
192instantiated to produce objects. To be able to instantiate objects,
193the constructors of a concrete class are needed. In SML, we can
194implement constructors as simple functions from arbitrary arguments to
195values of the interface type. Such a constructor function can
196encapsulate arbitrary private state and functions using lexical
197closure. It is also easy to share implementations of methods between
198two or more constructors.
199
200While the `Counter` example is rather trivial, it should not be
201difficult to see that this technique quite simply doesn't require a huge
202amount of extra verbiage and is more than usable in practice.
203
204
205== SML Modules and Dynamic Dispatch ==
206
207One might wonder about how SML modules and the dynamic dispatch
208technique work together. Let's investigate! Let's use a simple
209dispenser framework as a concrete example. (Note that this isn't
210intended to be an introduction to the SML module system.)
211
212=== Programming with SML Modules ===
213
214Using SML signatures we can specify abstract data types (ADTs) such as
215dispensers. Here is a signature for an "abstract" functional (as
216opposed to imperative) dispenser:
217
218[source,sml]
219----
220signature ABSTRACT_DISPENSER = sig
221 type 'a t
222 val isEmpty : 'a t -> bool
223 val push : 'a * 'a t -> 'a t
224 val pop : 'a t -> ('a * 'a t) option
225end
226----
227
228The term "abstract" in the name of the signature refers to the fact that
229the signature gives no way to instantiate a dispenser. It has nothing to
230do with the concept of abstract data types.
231
232Using SML functors we can write "generic" algorithms that manipulate
233dispensers of an unknown type. Here are a couple of very simple
234algorithms:
235
236[source,sml]
237----
238functor DispenserAlgs (D : ABSTRACT_DISPENSER) = struct
239 open D
240
241 fun pushAll (xs, d) = foldl push d xs
242
243 fun popAll d = let
244 fun lp (xs, NONE) = rev xs
245 | lp (xs, SOME (x, d)) = lp (x::xs, pop d)
246 in
247 lp ([], pop d)
248 end
249
250 fun cp (from, to) = pushAll (popAll from, to)
251end
252----
253
254As one can easily verify, the above compiles even without any concrete
255dispenser structure. Functors essentially provide a form a static
256dispatch that one can use to break compile-time dependencies.
257
258We can also give a signature for a concrete dispenser
259
260[source,sml]
261----
262signature DISPENSER = sig
263 include ABSTRACT_DISPENSER
264 val empty : 'a t
265end
266----
267
268and write any number of concrete structures implementing the signature.
269For example, we could implement stacks
270
271[source,sml]
272----
273structure Stack :> DISPENSER = struct
274 type 'a t = 'a list
275 val empty = []
276 val isEmpty = null
277 val push = op ::
278 val pop = List.getItem
279end
280----
281
282and queues
283
284[source,sml]
285----
286structure 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
293end
294----
295
296One can now write code that uses either the `Stack` or the `Queue`
297dispenser. One can also instantiate the previously defined functor to
298create functions for manipulating dispensers of a type:
299
300[source,sml]
301----
302structure S = DispenserAlgs (Stack)
303val [4,3,2,1] = S.popAll (S.pushAll ([1,2,3,4], Stack.empty))
304
305structure Q = DispenserAlgs (Queue)
306val [1,2,3,4] = Q.popAll (Q.pushAll ([1,2,3,4], Queue.empty))
307----
308
309There is no dynamic dispatch involved at the module level in SML. An
310attempt to do dynamic dispatch
311
312[source,sml]
313----
314val q = Q.push (1, Stack.empty)
315----
316
317will give a type error.
318
319=== Combining SML Modules and Dynamic Dispatch ===
320
321Let's then combine SML modules and the dynamic dispatch technique
322introduced in this article. First we define an interface for
323dispensers:
324
325[source,sml]
326----
327structure Dispenser = struct
328 datatype 'a t =
329 I of {isEmpty : unit -> bool,
330 push : 'a -> 'a t,
331 pop : unit -> ('a * 'a t) option}
332
333 fun O m (I t) = m t
334
335 fun isEmpty t = O#isEmpty t ()
336 fun push (v, t) = O#push t v
337 fun pop t = O#pop t ()
338end
339----
340
341The `Dispenser` module, which we can think of as an interface for
342dispensers, implements the `ABSTRACT_DISPENSER` signature using
343the dynamic dispatch technique, but we leave the signature ascription
344until later.
345
346Then we define a `DispenserClass` functor that makes a "class" out of
347a given dispenser module:
348
349[source,sml]
350----
351functor DispenserClass (D : DISPENSER) : DISPENSER = struct
352 open Dispenser
353
354 fun make d =
355 I {isEmpty = fn () => D.isEmpty d,
356 push = fn x => make (D.push (x, d)),
357 pop = fn () =>
358 case D.pop d of
359 NONE => NONE
360 | SOME (x, d) => SOME (x, make d)}
361
362 val empty =
363 I {isEmpty = fn () => true,
364 push = fn x => make (D.push (x, D.empty)),
365 pop = fn () => NONE}
366end
367----
368
369Finally we seal the `Dispenser` module:
370
371[source,sml]
372----
373structure Dispenser : ABSTRACT_DISPENSER = Dispenser
374----
375
376This isn't necessary for type safety, because the unsealed `Dispenser`
377module does not allow one to break encapsulation, but makes sure that
378only the `DispenserClass` functor can create dispenser classes
379(because the constructor `Dispenser.I` is no longer accessible).
380
381Using the `DispenserClass` functor we can turn any concrete dispenser
382module into a dispenser class:
383
384[source,sml]
385----
386structure StackClass = DispenserClass (Stack)
387structure QueueClass = DispenserClass (Queue)
388----
389
390Each dispenser class implements the same dynamic dispatch interface
391and the `ABSTRACT_DISPENSER` -signature.
392
393Because the dynamic dispatch `Dispenser` module implements the
394`ABSTRACT_DISPENSER`-signature, we can use it to instantiate the
395`DispenserAlgs`-functor:
396
397[source,sml]
398----
399structure D = DispenserAlgs (Dispenser)
400----
401
402The resulting `D` module, like the `Dispenser` module, works with
403any dispenser class and uses dynamic dispatch:
404
405[source,sml]
406----
407val [4, 3, 2, 1] = D.popAll (D.pushAll ([1, 2, 3, 4], StackClass.empty))
408val [1, 2, 3, 4] = D.popAll (D.pushAll ([1, 2, 3, 4], QueueClass.empty))
409----