Records and Objects

We will start by discussing records, a convenient way of arranging information together. Later on we combine records with closures and mutation to discuss concepts related to objects and object-oriented programming.

Records

A record is a bit like a tuple, except that each component is identified by a “name” instead, typically called a field. It must start with a lowercase letter. Here is an example:

{ a=4; b="hi!"; c=[1; 2] }

This record would have a record type:

{ a: int; b: string; c: int list }

One peculiar feature in OCAML is that a record type needs to be given a type alias first before a value of its type can be formed. So for example we can do:

type t = { a: int; b: string; c: int list }
let rcrd = { a=4; b="hi!"; c=[1; 2] }

Accessing the values in a record can be done in two ways. The one is a pattern match:

let { a=a; b=b; c=c } = rcrd in c
(* Convenient form when using var names same as fields *)
let { a; b; c } = rcrd in c

The other is with a “record access” notation:

rcrd.c

In general when records are concerned, it is best that their types are specified beforehand rather than inferred. For instance if all the information we have about a record is the expression rcrd.c, then all we know is that it has a field “c” and not much else. Type reconstruction becomes a bit harder. We shall revisit this issue when we discuss subtyping.

Objects

Even though OCAML natively supports objects, we will instead approach the subject from an exploratory direction: What are the key language constructs needed in order to have “objects”? What are objects anyway?

We can already do some of the above with the tools we have so far:

Here is perhaps the simplest object we can imagine, a “counter”:

type counterObj = { incr: unit -> unit; value: unit -> int; reset: unit -> unit }
let makeCounter init =
    let r = ref init
    in {
        incr = (fun () -> r := !r + 1);
        value = (fun () -> !r);
        reset = (fun () -> r := init)
    }

Here is an interaction with these objects:

let c1 = makeCounter 5
c1.value()     (* ---> 5 *)
c1.incr()
c1.value()     (* ---> 6 *)
c1.reset()
c1.value()     (* ---> 5 *)

Another key characteristic is that methods can call other methods from the object, possibly some defined in a superclass. We will see other ways of doing this in the future, but for now we can use recursion, and have the record we return be part of the closure of the functions in it:

type counterObj = { incr: unit -> unit; value: unit -> int;
                    add: int -> unit; reset: unit -> unit }
let makeCounter init =
    let r = ref init in
    let rec o = {
            incr = (fun () -> o.add 1);
            value = (fun () -> !r);
            add = (fun i -> r := !r + i);
            reset = (fun () -> r := init)
        }
    in o

We will revisit objects later when we discuss Racket.

Practice

  1. Create a constructor for “account” objects. An account object should keep a “balance” as, a floating point number, that is initialized to 0, and should allow one to “deposit” a positive amount or “withdraw” a positive amount as long as the balance would not go below 0. It should also have a “checkBalance” method to report the current balance.
  2. Create a constructor for “bank” objects. A bank object contains a list of accounts, starting with an empty list, and supports the following operations: