This is a quick demo of the object-oriented features of Objective Caml. Demonstrated a language with type inference is often frustrating, since the better the inference works the fewer types need to be shown. In the following we always print types of programs but the types themselves type inference will only be explained, or rather sketched in section X typechecking.
Traditionally, data is distinguished from functions operating on it. Data are integers, characters,
let integer = 1;; val integer : int = 1 let character = 'a';; val character : char = 'a' |
type stone = Opal | Pearl | Diamond;; type stone = | Opal | Pearl | Diamond type 'a list_ = Cons of 'a * 'a list_ | Nil;; type 'a list_ = | Cons of 'a * 'a list_ | Nil |
succ, pred;; - : (int -> int) * (int -> int) = <fun>, <fun> |
let print_integer n = print_int n; print_char ' ';; val print_integer : int -> unit = <fun> |
let grind = function
Opal -> ['O'; 'p'; 'a'; 'l'; ' ']
| Pearl -> ['P'; 'e'; 'a'; 'r'; 'l'; ' ' ]
| Diamond -> [ 'D'; 'i'; 'a'; 'm'; 'o'; 'n'; 'd'; ' '];;
val grind : stone -> char list = <fun>
|
let print_stone x = List.iter print_char (grind x);; val print_stone : stone -> unit = <fun> print_stone Opal;; Opal - : unit = () |
In the object oriented style, a piece of data is grouped with all the functions that may operate on that data in a structure called an object. Objects are built from classes. That is, classes abstract objects over the actual data they carry. For instance, here are two classes of integers and stones.
class integer n = object
val value = n
method zero = (value = 0)
method successor = {< value = value + 1 >}
method predecessor = {< value = min 0 (value-1) >}
method print = print_integer value
end;;
class integer :
int ->
object ('a)
val value : int
method predecessor : 'a
method print : unit
method successor : 'a
method zero : bool
end
class stone c = object
val matter = c
method grind = grind matter
method print = print_stone matter
end;;
class stone :
stone ->
object val matter : stone method grind : char list method print : unit end
|
let thirteen = new integer 13;; val thirteen : integer = <obj> let stone = new stone Diamond;; val stone : stone = <obj> |
thirteen # print; stone # print;; 13 Diamond - : unit = () |
let echo x = x # print; x # print;; val echo : < print : 'a; .. > -> 'a = <fun> |
echo thirteen; echo stone;; 13 13 Diamond Diamond - : unit = () |
Another essential feature of object-oriented languages is some inheritance mechanism that allows to defined new classes from older ones. This is illustrated below by defining a new class of jewels from the previous class of stones, adding a value field and a method price.
class jewel c v = object
inherit stone c as plain
val value = v
method price = 2 * value
method print =
plain # print;
print_int value;
print_string " carats"
end;;
class jewel :
stone ->
int ->
object
val matter : stone
val value : int
method grind : char list
method price : int
method print : unit
end
|
let solitaire = new jewel Diamond 13;; val solitaire : jewel = <obj> solitaire # print;; Diamond 13 carats- : unit = () |
Since recursive functions play an important role in the traditional programming style, methods soon also need to be recursive. This uses the so call self-binding mechanism. That is, the ability to bind the object receiving the message (to the variable game in the example below) and recursively invoke several messages of the object. We illustrate this on the famous roulette war game. A roulette game has a state position that remembers the slot carrying the unique bullet in the barrel. It has a method roulette that randomly turns the barrel, a method shoot, and two methods i_play and you_play for each of the players. A player shoots at himself, dies if the shoot fires; otherwise he forces the other play to play in turn. Thus, each of the player recursively calls the other one to play until one dies.
class roulette () = object (game)
val mutable position = 0
method roulette = position <- Random.int 8; game
method shoot = position <- (position + 1) mod 8; position = 0
method i_play = if game # shoot then "I die" else game # you_play
method you_play = if game # shoot then "You die" else game # i_play
end;;
class roulette :
unit ->
object ('a)
val mutable position : int
method i_play : string
method roulette : 'a
method shoot : bool
method you_play : string
end
|
(new roulette ()) # i_play;; - : string = "You die" |
(new roulette ()) # i_play;; - : string = "You die" |
(new roulette ()) # roulette # i_play;; - : string = "You die" |
The ability to send messages to self also provides late binding. That is, the recursive calls are not resolved when the class is defined (as they would by a ``let rec'' construction), but when objects of that class are actually created. This is illustrated in a specialization of the roulette game, when one player takes double risk. The implementation of the class roulette_2_for_1 inherits from the class roulette and redefines the method i_play to shoot twice (if not dead at the first shoot) before calling you_play.
class roulette_2_for_1 () = object (game)
inherit roulette ()
method me =
if game#shoot or game#shoot then "I die" else game # you_play
end;;
class roulette_2_for_1 :
unit ->
object ('a)
val mutable position : int
method i_play : string
method me : string
method roulette : 'a
method shoot : bool
method you_play : string
end
(new roulette_2_for_1 ()) # roulette # you_play;;
- : string = "You die"
|
The traditional programming style allows to define more functions without code duplication, but any change in the data representation requires redefinition of all functions operating on the same data-type.
The object-oriented style allows both the addition of operations (vertical extension) and the modification of the data-representation (horizontal extension) to be performed in a modular way. We shall illustrate this flexibility on the well-known lisp structure of cells. As opposed to the implementation of stones, we choose a fine-grain object-orientation flavor for cells. A cell is defined as a module will two classes. The main class cons defines real cells. The other defines the ``empty'' cell nil. It is important here that both classes have the same interface. Thus we defined the methods head and tail for object of class nil as raising exceptions. This should not be thought as a way around type safety, since in traditional ML one would also raise an exception when trying to access the head of an empty list. The module structure is used for sake of readability and to avoid name clashes, since we will consider several version of cells below.
module Cell = struct
class ['a, 'b] cons (h:'a) (t:'b) = object
val head = h
val tail = t
method null = false
method car = head
method cdr = tail
end
exception Null
class ['a, 'b] nil () = object
method null = true
method car = (raise Null : 'a)
method cdr = (raise Null : 'b)
end
end;;
module Cell :
sig
class ['a, 'b] cons :
'a ->
'b ->
object
val head : 'a
val tail : 'b
method car : 'a
method cdr : 'b
method null : bool
end
exception Null
class ['a, 'b] nil :
unit -> object method car : 'a method cdr : 'b method null : bool end
end
|
new Cell.cons thirteen solitaire;; - : (integer, jewel) Cell.cons = <obj> |
Let us make a detour though this amusing example of alternating lists that have been proposed by the Java community (it seems to have been a very challenging one). To increase readability, we define the type abbreviation ('a, 'b) alt_list for a recursive list structure whose elements are alternatively of type 'a and 'b. Then we simply define a recursive alternating list.
type ('a,'b) alt_list = ('a, ('b, ('a, 'b) alt_list) Cell.cons) Cell.cons;;
type ('a, 'b) alt_list = ('a, ('b, ('a, 'b) alt_list) Cell.cons) Cell.cons
let x = ref (new Cell.nil() : ('a, 'b) alt_list) in
x := new Cell.cons thirteen (new Cell.cons solitaire (!x)); !x;;
- : (integer, jewel) alt_list = <obj>
|
We recover traditional lists by enforcing cells to be of a recursive type. The following definition weakens the types but does not actually change the behavior.
module L = struct
class ['a] cons h t = object (self : 'mytype)
inherit ['a, 'mytype] Cell.cons h t
end
class ['a] nil () = object (self : 'mytype)
inherit ['a, 'mytype] Cell.nil ()
end
end;;
module L :
sig
class ['a] cons :
'a ->
'b ->
object ('b)
val head : 'a
val tail : 'b
method car : 'a
method cdr : 'b
method null : bool
end
class ['a] nil :
unit ->
object ('b) method car : 'a method cdr : 'b method null : bool end
end
|
In the object oriented style, we use inheritance to add new methods to classes. This can be done without redefining any old method. Moreover, using multiple inheritance, we first define several refinements independently and later provide different options by selecting different groups of refinements.
For instance, let us add the iteration to lists. The Iter module defines the iteration refinement for both cells and empty cells. It assumes two virtual methods car and cdr for the class cons, thus the class is declared virtual, since no instance can be taken until the class is combined with some other appropriate class providing the required methods.
module Iter = struct
class virtual ['a,'b] cons () = object (self)
method virtual car : 'a
method virtual cdr : 'b
method iter f =
(f self#car : unit); self#cdr#iter f; ()
end
class ['a] nil () = object
method iter (f : 'a -> unit) = ()
end
end;;
module Iter :
sig
class virtual ['a, 'b] cons :
unit ->
object
constraint 'b = < iter : ('a -> unit) -> 'c; .. >
method virtual car : 'a
method virtual cdr : 'b
method iter : ('a -> unit) -> unit
end
class ['a] nil : unit -> object method iter : ('a -> unit) -> unit end
end
|
class ['a] cons h t = object (self : 'mytype)
inherit ['a] L.cons h t
inherit ['a, 'mytype] Iter.cons ()
end
class ['a] nil () = object
inherit ['a] L.nil ()
inherit ['a] Iter.nil()
end;;
class ['a] cons :
'a ->
'b ->
object ('b)
val head : 'a
val tail : 'b
method car : 'a
method cdr : 'b
method iter : ('a -> unit) -> unit
method null : bool
end
class ['a] nil :
unit ->
object ('b)
method car : 'a
method cdr : 'b
method iter : ('a -> unit) -> unit
method null : bool
end
|
let primes =
List.fold_right
(new cons) [2;3;5;7;11;13] (new nil());;
val primes : int cons = <obj>
primes#iter print_integer;;
2 3 5 7 11 13 - : unit = ()
|
This other direction consists in adding a new constructor to an already existing data-types. Assume for instance that lists are often concatenated, and less often read. To avoid the computation of concatenation in the traditional style, one could add a new constructor Append to lists, creating a new incompatible type of lists. Therefore, this would necessitate to redefine all list operations.
In the object-oriented style, it is sufficient to define a new class append that implements the behavior of the new constructor. The older constructors are compatible and therefore remains unchanged.
class ['a] append l r = object (self : 'mytype)
val left = (l : 'mytype)
val right = (r : 'mytype)
method null = left # null && right # null
method car =
if left # null then right # car
else (left # car : 'a)
method cdr =
if left # null then right # cdr
else if left # cdr # null then right
else {< left = left # cdr >}
method iter (f : 'a -> unit) =
(left#iter f; right#iter f : unit)
end;;
class ['a] append :
'b ->
'b ->
object ('b)
val left : 'b
val right : 'b
method car : 'a
method cdr : 'b
method iter : ('a -> unit) -> unit
method null : bool
end
|
let (@@) = new append;; val @@ : 'a append -> 'a append -> 'a append = <fun> let double_primes = primes @@ (new nil() @@ new nil ()) @@ primes;; val double_primes : < car : int; cdr : 'a; iter : (int -> unit) -> unit; null : bool > as 'a = <obj> double_primes#iter print_integer;; 2 3 5 7 11 13 2 3 5 7 11 13 - : unit = () |
Typing is always a good thing. When programming with objects, this statement is reinforced by the complexity of recursive calls via late binding.
In Objective Caml, typechecking heavily relies on row variables. Row variables provide polymorphic messages. When typing messages, constraints on the existence of methods and their types are gathered in the type of the object.
let g x = if x # null then x # car else x # cdr;; val g : < car : 'a; cdr : 'a; null : bool; .. > -> 'a = <fun> |
echo;;
- : < print : 'a; .. > -> 'a = <fun>
echo stone;;
Diamond Diamond - : unit = ()
echo primes;;
Characters 5-11:
This expression has type
int cons =
< car : int; cdr : int cons; iter : (int -> unit) -> unit; null : bool >
but is here used with type < print : 'a; .. >
Only the second object type has a method print
|
Explicitly typed languages often use subtyping polymorphism instead of parametric polymorphism to type messages. To illustrate subtyping, we must first turn off polymorphism artificially using a type constraint. The monomorphic echo function can be applied to stones but not to jewels anymore.
let monomorphic_echo (x : stone) = echo x;; val monomorphic_echo : stone -> unit = <fun> monomorphic_echo stone;; Diamond Diamond - : unit = () monomorphic_echo solitaire;; Characters 17-26: This expression has type jewel = < grind : char list; price : int; print : unit > but is here used with type stone = < grind : char list; print : unit > Only the first object type has a method price |
let as_a_stone x = (x :> stone);; val as_a_stone : < grind : char list; print : unit; .. > -> stone = <fun> |
monomorphic_echo (as_a_stone solitaire);; Diamond 13 caratsDiamond 13 carats- : unit = () monomorphic_echo (solitaire :> stone);; Diamond 13 caratsDiamond 13 carats- : unit = () |
Typing recursion is often hard to get right. Actually, since Ocaml uses parametric polymorphism instead of subtyping, all difficulties are avoided. We summarize all kinds of operations involving self and recursion in a demoniac class.
class demon () = object (it_self)
val mutable genes = Random.int 9999999
method identity = genes
method self = it_self
method clone = {< >}
method reproduction = {< genes = genes + 1 >}
method mutation = genes <- genes + Random.int 9999999
end;;
class demon :
unit ->
object ('a)
val mutable genes : int
method clone : 'a
method identity : int
method mutation : unit
method reproduction : 'a
method self : 'a
end
|
let dolly = new demon();; val dolly : demon = <obj> dolly = dolly # self;; - : bool = true dolly <> dolly # clone;; - : bool = true let id = dolly # identity in dolly # reproduction; id = dolly # identity;; - : bool = true let id = dolly # identity in dolly # mutation; id <> dolly # identity;; - : bool = true |
module D = struct
class ['a] cons h t = object
inherit demon()
inherit ['a] L.cons h t
end
class ['a] nil () = object
inherit demon()
inherit ['a] L.nil ()
end
end;;
module D :
sig
class ['a] cons :
'a ->
'b ->
object ('b)
val mutable genes : int
val head : 'a
val tail : 'b
method car : 'a
method cdr : 'b
method clone : 'b
method identity : int
method mutation : unit
method null : bool
method reproduction : 'b
method self : 'b
end
class ['a] nil :
unit ->
object ('b)
val mutable genes : int
method car : 'a
method cdr : 'b
method clone : 'b
method identity : int
method mutation : unit
method null : bool
method reproduction : 'b
method self : 'b
end
end
|
let experiment = new D.cons dolly (new D.nil());; val experiment : demon D.cons = <obj> experiment # self # reproduction # clone # self # car # identity;; - : int = 8346501 |
class compare () = object (self)
method compare x = compare x self
end;;
class compare : unit -> object ('a) method compare : 'a -> int end
|
class jeweler c v = object
inherit jewel c v
inherit compare ()
end;;
class jeweler :
stone ->
int ->
object ('a)
val matter : stone
val value : int
method compare : 'a -> int
method grind : char list
method price : int
method print : unit
end
|
For completeness, we end this demo with a few examples that Ocaml cannot type yet in its current implementation.
We have shown how to equipped lists with iterators. Of course, one would also want to equipped them with folding methods. This, however requires polymorphic methods, which Ocaml does not yet allow. Indeed, a methods are part of objects which are first class values, and thus are not allowed to be polymorphic. Fortunately, some recent result would allow to extend Ocaml with some form of first class polymorphism and define the following class
module P = struct
class ['a] cons h t = object (self)
inherit ['a,'h] L.cons h t
method (fold : All 'b. ('a -> 'b -> 'b) -> 'b -> 'b) f x =
self#cdr#fold (f self#car)
end
class ['a] nil () = object
inherit ('a) L.nil ()
method (fold : All 'b. ('a -> 'b -> 'b) -> 'b -> 'b) f x = x
end
end;;
Characters 98-99:
Syntax error
|
Fields of objects can be hidden at any time. Methods, however, can only be hidden if they have been declared private when first introduced. This guarantees that they have never been assumed visible in self. This is a limitation to modularity that we would like to remove.
Because Ocaml has both a powerful module system and powerful object operations, one often has a choice of programming style. Traditionally, one would write a stack as a module.
module Stack = struct
exception Empty
let create () = ref []
let push s x = s := x::!s
let pop s = match !s with [] -> raise Empty | a::l' -> s := l'; a
let clear s = s := []
let length s = List.length !s
end;;
module Stack :
sig
exception Empty
val create : unit -> 'a list ref
val push : 'a list ref -> 'a -> unit
val pop : 'a list ref -> 'a
val clear : 'a list ref -> unit
val length : 'a list ref -> int
end
|
module Stack' = struct
exception Empty
class ['a] stack () = object
val mutable l = ([] : 'a list)
method push x = l <- x::l
method pop = match l with [] -> raise Empty | a::l' -> l <- l'; a
method clear = l <- []
method length = List.length l
end
end;;
module Stack' :
sig
exception Empty
class ['a] stack :
unit ->
object
val mutable l : 'a list
method clear : unit
method length : int
method pop : 'a
method push : 'a -> unit
end
end
|
In summary: