Objects
This chapter was written by Leo White and Jason Hickey.
We’ve already seen several tools that OCaml provides for organizing programs, particularly modules. In addition, OCaml also supports object-oriented programming. There are objects, classes, and their associated types. In this chapter, we’ll introduce you to OCaml objects and subtyping. In the next chapter, Chapter 13, Classes, we’ll introduce you to classes and inheritance.
What Is Object-Oriented Programming?
Object-oriented programming (often shortened to OOP) is a programming style that encapsulates computation and data within logical objects. Each object contains some data stored in fields and has method functions that can be invoked against the data within the object (also called “sending a message” to the object). The code definition behind an object is called a class, and objects are constructed from a class definition by calling a constructor with the data that the object will use to build itself.
There are five fundamental properties that differentiate OOP from other styles:
- Abstraction
- The details of the implementation are hidden in the object, and the external interface is just the set of publicly accessible methods.
- Dynamic lookup
- When a message is sent to an object, the method to be executed is determined by the implementation of the object, not by some static property of the program. In other words, different objects may react to the same message in different ways.
- Subtyping
- If an object
a
has all the functionality of an objectb
, then we may usea
in any context whereb
is expected. - Inheritance
- The definition of one kind of object can be reused to produce a new kind of object. This new definition can override some behavior, but also share code with its parent.
- Open recursion
- An object’s methods can invoke another method in the same object using a special variable (often called
self
orthis
). When objects are created from classes, these calls use dynamic lookup, allowing a method defined in one class to invoke methods defined in another class that inherits from the first.
Almost every notable modern programming language has been influenced by OOP, and you’ll have run across these terms if you’ve ever used C++, Java, C#, Ruby, Python, or JavaScript.
OCaml Objects
If you already know about object-oriented programming in a language like Java or C++, the OCaml object system may come as a surprise. Foremost is the complete separation of objects and their types from the class system. In a language like Java, a class name is also used as the type of objects created by instantiating it, and the relationships between these object types correspond to inheritance. For example, if we implement a class Deque
in Java by inheriting from a class Stack
, we would be allowed to pass a deque anywhere a stack is expected.
OCaml is entirely different. Classes are used to construct objects and support inheritance, but classes are not types. Instead, objects have object types, and if you want to use objects, you aren’t required to use classes at all. Here’s an example of a simple object:
open Base;;
let s = object
val mutable v = [0; 2]
method pop =
match v with
| hd :: tl ->
v <- tl;
Some hd
| [] -> None
method push hd =
v <- hd :: v
end;;
>val s : < pop : int option; push : int -> unit > = <obj>
The object has an integer list value v
, a method pop
that returns the head of v
, and a method push
that adds an integer to the head of v
.
The object type is enclosed in angle brackets < ... >
, containing just the types of the methods. Fields, like v
, are not part of the public interface of an object. All interaction with an object is through its methods. The syntax for a method invocation uses the #
character:
s#pop;;
>- : int option = Some 0
s#push 4;;
>- : unit = ()
s#pop;;
>- : int option = Some 4
Note that unlike functions, methods can have zero parameters, since the method call is routed to a concrete object instance. That’s why the pop
method doesn’t have a unit
argument, as the equivalent functional version would.
Objects can also be constructed by functions. If we want to specify the initial value of the object, we can define a function that takes the value and returns an object:
let stack init = object
val mutable v = init
method pop =
match v with
| hd :: tl ->
v <- tl;
Some hd
| [] -> None
method push hd =
v <- hd :: v
end;;
>val stack : 'a list -> < pop : 'a option; push : 'a -> unit > = <fun>
let s = stack [3; 2; 1];;
>val s : < pop : int option; push : int -> unit > = <obj>
s#pop;;
>- : int option = Some 3
Note that the types of the function stack
and the returned object now use the polymorphic type 'a
. When stack
is invoked on a concrete value [3; 2; 1]
, we get the same object type as before, with type int
for the values on the stack.
Object Polymorphism
Like polymorphic variants, methods can be used without an explicit type declaration:
let area sq = sq#width * sq#width;;
>val area : < width : int; .. > -> int = <fun>
let minimize sq : unit = sq#resize 1;;
>val minimize : < resize : int -> unit; .. > -> unit = <fun>
let limit sq = if (area sq) > 100 then minimize sq;;
>val limit : < resize : int -> unit; width : int; .. > -> unit = <fun>
As you can see, object types are inferred automatically from the methods that are invoked on them.
The type system will complain if it sees incompatible uses of the same method:
let toggle sq b : unit =
if b then sq#resize `Fullscreen else minimize sq;;
>Line 2, characters 51-53:
>Error: This expression has type < resize : [> `Fullscreen ] -> unit; .. >
> but an expression was expected of type < resize : int -> unit; .. >
> Types for method resize are incompatible
The ..
in the inferred object types are ellipses, standing for other unspecified methods that the object may have. The type < width : float; .. >
specifies an object that must have at least a width
method, and possibly some others as well. Such object types are said to be open.
We can manually close an object type using a type annotation:
let area_closed (sq: < width : int >) = sq#width * sq#width;;
>val area_closed : < width : int > -> int = <fun>
let sq = object
method width = 30
method name = "sq"
end;;
>val sq : < name : string; width : int > = <obj>
area_closed sq;;
>Line 1, characters 13-15:
>Error: This expression has type < name : string; width : int >
> but an expression was expected of type < width : int >
> The second object type has no method name
Elisions Are Polymorphic
The ..
in an open object type is an elision, standing for “possibly more methods.” It may not be apparent from the syntax, but an elided object type is actually polymorphic. For example, if we try to write a type definition, we get an “unbound type variable” error:
type square = < width : int; ..>;;
>Line 1, characters 1-33:
>Error: A type variable is unbound in this type declaration.
> In type < width : Base.int; .. > as 'a the variable 'a is unbound
This is because ..
is really a special kind of type variable called a row variable.
This kind of typing scheme using row variables is called row polymorphism. Row polymorphism is also used in polymorphic variant types, and there is a close relationship between objects and polymorphic variants: objects are to records what polymorphic variants are to ordinary variants.
An object of type < pop : int option; .. >
can be any object with a method pop : int option
; it doesn’t matter how it is implemented. When the method #pop
is invoked, the actual method that is run is determined by the object. Consider the following function.
let print_pop st = Option.iter ~f:(Stdio.printf "Popped: %d\n") st#pop;;
>val print_pop : < pop : int option; .. > -> unit = <fun>
We can run it on the stack type we defined above, which is based on linked lists.
print_pop (stack [5;4;3;2;1]);;
>Popped: 5
>- : unit = ()
But we could also create a totally different implementation of stacks, using Base’s array-based Stack
module.
let array_stack l = object
val stack = Stack.of_list l
method pop = Stack.pop stack
end;;
>val array_stack : 'a list -> < pop : 'a option > = <fun>
And print_pop
will work just as well on this kind of stack object, despite having a completely different implementation.
print_pop (array_stack [5;4;3;2;1]);;
>Popped: 5
>- : unit = ()
Immutable Objects
Many people consider object-oriented programming to be intrinsically imperative, where an object is like a state machine. Sending a message to an object causes it to change state, possibly sending messages to other objects.
Indeed, in many programs this makes sense, but it is by no means required. Let’s define a function that creates immutable stack objects:
let imm_stack init = object
val v = init
method pop =
match v with
| hd :: tl -> Some (hd, {< v = tl >})
| [] -> None
method push hd =
{< v = hd :: v >}
end;;
>val imm_stack :
> 'a list -> (< pop : ('a * 'b) option; push : 'a -> 'b > as 'b) = <fun>
The key parts of this implementation are in the pop
and push
methods. The expression {< ... >}
produces a copy of the current object, with the same type, and the specified fields updated. In other words, the push hd
method produces a copy of the object, with v
replaced by hd :: v
. The original object is not modified:
let s = imm_stack [3; 2; 1];;
>val s : < pop : (int * 'a) option; push : int -> 'a > as 'a = <obj>
let r = s#push 4;;
>val r : < pop : (int * 'a) option; push : int -> 'a > as 'a = <obj>
s#pop;;
>- : (int * (< pop : 'a; push : int -> 'b > as 'b)) option as 'a =
>Some (3, <obj>)
r#pop;;
>- : (int * (< pop : 'a; push : int -> 'b > as 'b)) option as 'a =
>Some (4, <obj>)
There are some restrictions on the use of the expression {< ... >}
. It can be used only within a method body, and only the values of fields may be updated. Method implementations are fixed at the time the object is created; they cannot be changed dynamically.
When to Use Objects
You might wonder when to use objects in OCaml, which has a multitude of alternative mechanisms to express similar concepts. First-class modules are more expressive (a module can include types, while classes and objects cannot). Modules, functors, and data types also offer a wide range of ways to express program structure. In fact, many seasoned OCaml programmers rarely use classes and objects, if at all.
Objects have some advantages over records: they don’t require type definitions, and their support for row polymorphism makes them more flexible. However, the heavy syntax and additional runtime cost means that objects are rarely used in place of records.
The real benefits of objects come from the class system. Classes support inheritance and open recursion. Open recursion allows interdependent parts of an object to be defined separately. This works because calls between the methods of an object are determined when the object is instantiated, a form of late binding. This makes it possible (and necessary) for one method to refer to other methods in the object without knowing statically how they will be implemented.
In contrast, modules use early binding. If you want to parameterize your module code so that some part of it can be implemented later, you would write a function or functor. This is more explicit, but often more verbose than overriding a method in a class.
In general, a rule of thumb is: use classes and objects in situations where open recursion is a big win. Two good examples are Xavier Leroy’s Cryptokit, which provides a variety of cryptographic primitives that can be combined in building-block style; and the Camlimages library, which manipulates various graphical file formats. Camlimages also provides a module-based version of the same library, letting you choose between functional and object-oriented styles depending on your problem domain.
We’ll introduce you to classes, and examples using open recursion, in Chapter 13, Classes.
Subtyping
Subtyping is a central concept in object-oriented programming. It governs when an object with one type A can be used in an expression that expects an object of another type B. When this is true, we say that A is a subtype of B. More concretely, subtyping restricts when the coercion operator e :> t
can be applied. This coercion works only if the type of e
is a subtype of t
.
Width Subtyping
To explore this, let’s define some simple object types for geometric shapes. The generic type shape
just has a method to compute the area.
type shape = < area : float >;;
>type shape = < area : float >
Now let’s add a type representing a specific kind of shape, as well as a function for creating objects of that type.
type square = < area : float; width : int >;;
>type square = < area : float; width : int >
let square w = object
method area = Float.of_int (w * w)
method width = w
end;;
>val square : int -> < area : float; width : int > = <fun>
A square
has a method area
just like a shape
, and an additional method width
. Still, we expect a square
to be a shape
, and it is. Note, however, that the coercion :>
must be explicit:
(square 10 : shape);;
>Line 1, characters 2-11:
>Error: This expression has type < area : float; width : int >
> but an expression was expected of type shape
> The second object type has no method width
(square 10 :> shape);;
>- : shape = <obj>
This form of object subtyping is called width subtyping. Width subtyping means that an object type A is a subtype of B, if A has all of the methods of B, and possibly more. A square
is a subtype of shape
because it implements all of the methods of shape
, which in this case means the area
method.
Depth Subtyping
We can also use depth subtyping with objects. Depth subtyping allows us to coerce an object if its individual methods could safely be coerced. So an object type < m: t1 >
is a subtype of < m: t2 >
if t1
is a subtype of t2
.
First, let’s add a new shape type, circle
:
type circle = < area : float; radius : int >;;
>type circle = < area : float; radius : int >
let circle r = object
method area = 3.14 *. (Float.of_int r) **. 2.0
method radius = r
end;;
>val circle : int -> < area : float; radius : int > = <fun>
Using that, let’s create a couple of objects that each have a shape
method, one returning a shape of type circle
:
let coin = object
method shape = circle 5
method color = "silver"
end;;
>val coin : < color : string; shape : < area : float; radius : int > > = <obj>
And the other returning a shape of type square
:
let map = object
method shape = square 10
end;;
>val map : < shape : < area : float; width : int > > = <obj>
Both these objects have a shape
method whose type is a subtype of the shape
type, so they can both be coerced into the object type < shape : shape >
:
type item = < shape : shape >;;
>type item = < shape : shape >
let items = [ (coin :> item) ; (map :> item) ];;
>val items : item list = [<obj>; <obj>]
Polymorphic Variant Subtyping
Subtyping can also be used to coerce a polymorphic variant into a larger polymorphic variant type. A polymorphic variant type A is a subtype of B, if the tags of A are a subset of the tags of B:
type num = [ `Int of int | `Float of float ];;
>type num = [ `Float of float | `Int of int ]
type const = [ num | `String of string ];;
>type const = [ `Float of float | `Int of int | `String of string ]
let n : num = `Int 3;;
>val n : num = `Int 3
let c : const = (n :> const);;
>val c : const = `Int 3
Variance
What about types built from object types? If a square
is a shape
, we expect a square list
to be a shape list
. OCaml does indeed allow such coercions:
let squares: square list = [ square 10; square 20 ];;
>val squares : square list = [<obj>; <obj>]
let shapes: shape list = (squares :> shape list);;
>val shapes : shape list = [<obj>; <obj>]
Note that this relies on lists being immutable. It would not be safe to treat a square array
as a shape array
because it would allow you to store non-square shapes into what should be an array of squares. OCaml recognizes this and does not allow the coercion:
let square_array: square array = [| square 10; square 20 |];;
>val square_array : square array = [|<obj>; <obj>|]
let shape_array: shape array = (square_array :> shape array);;
>Line 1, characters 32-61:
>Error: Type square array is not a subtype of shape array
> The second object type has no method width
We say that 'a list
is covariant (in 'a
), while 'a array
is invariant.
Subtyping function types requires a third class of variance. A function with type square -> string
cannot be used with type shape -> string
because it expects its argument to be a square
and would not know what to do with a circle
. However, a function with type shape -> string
can safely be used with type square -> string
:
let shape_to_string: shape -> string =
fun s -> Printf.sprintf "Shape(%F)" s#area;;
>val shape_to_string : shape -> string = <fun>
let square_to_string: square -> string =
(shape_to_string :> square -> string);;
>val square_to_string : square -> string = <fun>
We say that 'a -> string
is contravariant in 'a
. In general, function types are contravariant in their arguments and covariant in their results.
Variance Annotations
OCaml works out the variance of a type using that type’s definition. Consider the following simple immutable Either
type.
module Either = struct
type ('a, 'b) t =
| Left of 'a
| Right of 'b
let left x = Left x
let right x = Right x
end;;
>module Either :
> sig
> type ('a, 'b) t = Left of 'a | Right of 'b
> val left : 'a -> ('a, 'b) t
> val right : 'a -> ('b, 'a) t
> end
By looking at what coercions are allowed, we can see that the type parameters of the immutable Either
type are covariant.
let left_square = Either.left (square 40);;
>val left_square : (< area : float; width : int >, 'a) Either.t =
> Either.Left <obj>
(left_square :> (shape,_) Either.t);;
>- : (shape, 'a) Either.t = Either.Left <obj>
The story is different, however, if the definition is hidden by a signature.
module Abs_either : sig
type ('a, 'b) t
val left: 'a -> ('a, 'b) t
val right: 'b -> ('a, 'b) t
end = Either;;
>module Abs_either :
> sig
> type ('a, 'b) t
> val left : 'a -> ('a, 'b) t
> val right : 'b -> ('a, 'b) t
> end
In this case, OCaml is forced to assume that the type is invariant.
(Abs_either.left (square 40) :> (shape, _) Abs_either.t);;
>Line 1, characters 2-29:
>Error: This expression cannot be coerced to type (shape, 'b) Abs_either.t;
> it has type (< area : float; width : int >, 'a) Abs_either.t
> but is here used with type (shape, 'b) Abs_either.t
> Type < area : float; width : int > is not compatible with type
> shape = < area : float >
> The second object type has no method width
We can fix this by adding variance annotations to the type’s parameters in the signature: +
for covariance or -
for contravariance:
module Var_either : sig
type (+'a, +'b) t
val left: 'a -> ('a, 'b) t
val right: 'b -> ('a, 'b) t
end = Either;;
>module Var_either :
> sig
> type (+'a, +'b) t
> val left : 'a -> ('a, 'b) t
> val right : 'b -> ('a, 'b) t
> end
As you can see, this now allows the coercion once again.
(Var_either.left (square 40) :> (shape, _) Var_either.t);;
>- : (shape, 'a) Var_either.t = <abstr>
For a more concrete example of variance, let’s create some stacks containing shapes by applying our stack
function to some squares and some circles:
type 'a stack = < pop: 'a option; push: 'a -> unit >;;
>type 'a stack = < pop : 'a option; push : 'a -> unit >
let square_stack: square stack = stack [square 30; square 10];;
>val square_stack : square stack = <obj>
let circle_stack: circle stack = stack [circle 20; circle 40];;
>val circle_stack : circle stack = <obj>
If we wanted to write a function that took a list of such stacks and found the total area of their shapes, we might try:
let total_area (shape_stacks: shape stack list) =
let stack_area acc st =
let rec loop acc =
match st#pop with
| Some s -> loop (acc +. s#area)
| None -> acc
in
loop acc
in
List.fold ~init:0.0 ~f:stack_area shape_stacks;;
>val total_area : shape stack list -> float = <fun>
However, when we try to apply this function to our objects, we get an error:
total_area [(square_stack :> shape stack); (circle_stack :> shape stack)];;
>Line 1, characters 13-42:
>Error: Type square stack = < pop : square option; push : square -> unit >
> is not a subtype of
> shape stack = < pop : shape option; push : shape -> unit >
> Type shape = < area : float > is not a subtype of
> square = < area : float; width : int >
> The first object type has no method width
As you can see, square stack
and circle stack
are not subtypes of shape stack
. The problem is with the push
method. For shape stack
, the push
method takes an arbitrary shape
. So if we could coerce a square stack
to a shape stack
, then it would be possible to push an arbitrary shape onto square stack
, which would be an error.
Another way of looking at this is that < push: 'a -> unit; .. >
is contravariant in 'a
, so < push: square -> unit; pop: square option >
cannot be a subtype of < push: shape -> unit; pop: shape option >
.
Still, the total_area
function should be fine, in principle. It doesn’t call push
, so it isn’t making that error. To make it work, we need to use a more precise type that indicates we are not going to be using the push
method. We define a type readonly_stack
and confirm that we can coerce the list of stack
s to it:
type 'a readonly_stack = < pop : 'a option >;;
>type 'a readonly_stack = < pop : 'a option >
let total_area (shape_stacks: shape readonly_stack list) =
let stack_area acc st =
let rec loop acc =
match st#pop with
| Some s -> loop (acc +. s#area)
| None -> acc
in
loop acc
in
List.fold ~init:0.0 ~f:stack_area shape_stacks;;
>val total_area : shape readonly_stack list -> float = <fun>
total_area [(square_stack :> shape readonly_stack); (circle_stack :>
shape readonly_stack)];;
>- : float = 7280.
Aspects of this section may seem fairly complicated, but it should be pointed out that this typing works, and in the end, the type annotations are fairly minor. In most typed object-oriented languages, these coercions would simply not be possible. For example, in C++, a STL type list<T>
is invariant in T
, so it is simply not possible to use list<square>
where list<shape>
is expected (at least safely). The situation is similar in Java, although Java has an escape hatch that allows the program to fall back to dynamic typing. The situation in OCaml is much better: it works, it is statically checked, and the annotations are pretty simple.
Narrowing
Narrowing, also called down casting, is the ability to coerce an object to one of its subtypes. For example, if we have a list of shapes shape list
, we might know (for some reason) what the actual type of each shape is. Perhaps we know that all objects in the list have type square
. In this case, narrowing would allow the recasting of the object from type shape
to type square
. Many languages support narrowing through dynamic type checking. For example, in Java, a coercion (Square) x
is allowed if the value x
has type Square
or one of its subtypes; otherwise the coercion throws an exception.
Narrowing is not permitted in OCaml. Period.
Why? There are two reasonable explanations, one based on a design principle, and another technical (the technical reason is simple: it is hard to implement).
The design argument is this: narrowing violates abstraction. In fact, with a structural typing system like in OCaml, narrowing would essentially provide the ability to enumerate the methods in an object. To check whether an object obj
has some method foo : int
, one would attempt a coercion (obj :> < foo : int >)
.
More pragmatically, narrowing leads to poor object-oriented style. Consider the following Java code, which returns the name of a shape object:
String GetShapeName(Shape s) {
if (s instanceof Square) {
return "Square";
} else if (s instanceof Circle) {
return "Circle";
} else {
return "Other";
}
}
Most programmers would consider this code to be awkward, at the least. Instead of performing a case analysis on the type of object, it would be better to define a method to return the name of the shape. Instead of calling GetShapeName(s)
, we should call s.Name()
instead.
However, the situation is not always so obvious. The following code checks whether an array of shapes looks like a barbell, composed of two Circle
objects separated by a Line
, where the circles have the same radius:
boolean IsBarbell(Shape[] s) {
return s.length == 3 && (s[0] instanceof Circle) &&
(s[1] instanceof Line) && (s[2] instanceof Circle) &&
((Circle) s[0]).radius() == ((Circle) s[2]).radius();
}
In this case, it is much less clear how to augment the Shape
class to support this kind of pattern analysis. It is also not obvious that object-oriented programming is well-suited for this situation. Pattern matching seems like a better fit:
type shape = Circle of { radius : int } | Line of { length: int };;
>type shape = Circle of { radius : int; } | Line of { length : int; }
let is_barbell = function
| [Circle {radius=r1}; Line _; Circle {radius=r2}] when r1 = r2 -> true
| _ -> false;;
>val is_barbell : shape list -> bool = <fun>
Regardless, there is a solution if you find yourself in this situation, which is to augment the classes with variants. You can define a method variant
that injects the actual object into a variant type.
type shape = < variant : repr >
and circle = < variant : repr; radius : int >
and line = < variant : repr; length : int >
and repr =
| Circle of circle
| Line of line;;
>type shape = < variant : repr >
>and circle = < radius : int; variant : repr >
>and line = < length : int; variant : repr >
>and repr = Circle of circle | Line of line
let is_barbell = function
| [s1; s2; s3] ->
(match s1#variant, s2#variant, s3#variant with
| Circle c1, Line _, Circle c2 when c1#radius = c2#radius -> true
| _ -> false)
| _ -> false;;
>val is_barbell : < variant : repr; .. > list -> bool = <fun>
This pattern works, but it has drawbacks. In particular, the recursive type definition should make it clear that this pattern is essentially equivalent to using variants, and that objects do not provide much value here.
Subtyping Versus Row Polymorphism
There is considerable overlap between subtyping and row polymorphism. Both mechanisms allow you to write functions that can be applied to objects of different types. In these cases, row polymorphism is usually preferred over subtyping because it does not require explicit coercions, and it preserves more type information, allowing functions like the following:
let remove_large l =
List.filter ~f:(fun s -> Float.(s#area <= 100.)) l;;
>val remove_large : (< area : float; .. > as 'a) list -> 'a list = <fun>
The return type of this function is built from the open object type of its argument, preserving any additional methods that it may have, as we can see below.
let squares : < area : float; width : int > list =
[square 5; square 15; square 10];;
>val squares : < area : float; width : int > list = [<obj>; <obj>; <obj>]
remove_large squares;;
>- : < area : float; width : int > list = [<obj>; <obj>]
Writing a similar function with a closed type and applying it using subtyping does not preserve the methods of the argument: the returned object is only known to have an area
method:
let remove_large (l: < area : float > list) =
List.filter ~f:(fun s -> Float.(s#area <= 100.)) l;;
>val remove_large : < area : float > list -> < area : float > list = <fun>
remove_large (squares :> < area : float > list );;
>- : < area : float > list = [<obj>; <obj>]
There are some situations where we cannot use row polymorphism. In particular, row polymorphism cannot be used to place different types of objects in the same container. For example, lists of heterogeneous elements cannot be created using row polymorphism:
let hlist: < area: float; ..> list = [square 10; circle 30];;
>Line 1, characters 50-59:
>Error: This expression has type < area : float; radius : int >
> but an expression was expected of type < area : float; width : int >
> The second object type has no method radius
Similarly, we cannot use row polymorphism to store different types of object in the same reference:
let shape_ref: < area: float; ..> ref = ref (square 40);;
>val shape_ref : < area : float; width : int > ref =
> {Base.Ref.contents = <obj>}
shape_ref := circle 20;;
>Line 1, characters 14-23:
>Error: This expression has type < area : float; radius : int >
> but an expression was expected of type < area : float; width : int >
> The second object type has no method radius
In both these cases we must use subtyping:
let hlist: shape list = [(square 10 :> shape); (circle 30 :> shape)];;
>val hlist : shape list = [<obj>; <obj>]
let shape_ref: shape ref = ref (square 40 :> shape);;
>val shape_ref : shape ref = {Base.Ref.contents = <obj>}
shape_ref := (circle 20 :> shape);;
>- : unit = ()