In the first article we covered the basics of objects in ReasonML: how to define objects, classes, mutation, and encapsulation using private methods.

The Polymorphic Nature of Objects

Polymorphism is that ability for an object or method to take many forms. Essentially a function or method is polymorphic when it can take more than one type of parameter.  

There is an interesting feature of objects where – unlike records – any object that matches the signature of a function is deemed acceptable, this makes writing polymorphism easy to achieve.  Note that can even be as undesirable, as it means methods or functions taking objects not as strict as with records: the wrong object could be passed in and the compiler may not help you!

We can create polymophic functions by using an object type as the parameter for a method. So lets create a simple type:

type pet = {
  .
  getName: string,
  talk: unit
}

Then you are free to implement this type as many times as you like.

class cat = (name) => {
  as _;
  pub getName = String.trim(name);
  pub talk =  Js.log("Nya!");
};

class dog = (name) => {
  as _;
  pub getName = String.capitalize_ascii(name);
  pub talk =  Js.log2("Woof!", name);
};

As both the cat and dog classes implement the pet signature, we can write a function that works on pet and it will accept both cats and dogs:

let myCat: cat = new cat(" Cuddles ");
let myDog: dog = new dog("spot");
let greet: pet => string = 
	p => "Hello " ++ p#getName;
let doubleGreet: string = 
	greet(myCat) ++ " and " ++ greet(myDog);

Abstraction using Virtual Classes

Inheritance in ReasonML can be defined using virtual objects and methods. Essentially a virtual method or object is similar to an abstract method or class in Java — it is left undefined and has to be implemented by the inheriting class.

We can define a virtual class as follows:

class virtual pet (name: string) = {
  as self;
  pri getName = 
    name 
    |> String.trim 
    |> String.capitalize_ascii;
  pub virtual talk: unit;
};

// You can't do this:
let myVirtualPet = new pet("davey");

There a few things to take note of here:

  • If class is defined as virtual, we cannot instantiate it.
  • A virtual class may contain private methods. These act like protected methods in Java. When defining a subclass, we will have access to them; however, once we instantiate a class we will not have access to it. This is a form of abstraction.
  • You can define concrete methods alongside virtual methods. In this case getName handles trimming and capitalising for us and the user no-longer has to define that functionality per class.

Inheriting from a Virtual Class

Instead of creating virtual classes, we have to define a new class that inherits from the virtual class. Note how in this instance we are able to access the getName private method from within the class definition; however, this cannot be accessed from a dog or pet object.

class dog (name) = {
  as self;
  inherit (class pet)(name);
  pub talk = 
    Js.log(self#getName ++ ", says Woof!");
};


let myDog: pet = new dog("Spot");
myDog#talk; // prints 'Spot, says Woof!'
myDog#getName; // does not compile :(
class cat (name) = {
  as _;
  inherit (class pet)(name);
  pub talk = "Nya!");
};
let myCat: pet = new cat("Kitten");
myCat#talk; // prints 'Nya!'

As you can see the talk method is implemented differently for both instances. Thus a pet has polymophic behaviour, unless we know which instance it actually is we are unable to determine how it behaves.

Overriding a Method

Sometimes you wish to override the functionality provided by the class you are inheriting from. This can be done by adding an exclamation mark after inherit , like so:

class cat (name) = {
  as self;
  inherit (class pet)(name);
  pub talk = 
    Js.log(self#getName ++ ", says Nya!");
};
class shyCat (name) = {
  as self;
  inherit (class cat)(name);
  pub! talk = Js.log(self#getName);
};

In the above example, shyCat ‘s talk method overrides cat to not include the cat’s name.

Multiple Inheritance and the Diamond Problem

Interestingly OCaml supports multiple inheritance. This is something that often needs to be treated with care as there is a chance you may run into the diamond problem:

An ambiguity that arises when two classes B and C inherit from A, and class D inherits from both B and C. If there is a method in A that B and C have overridden, and D does not override it, then which version of the method does D inherit: that of B, or that of C?

Thankfully OCaml mitigates this by enforcing that you define the inheritance in order. Thus sub-classes will take the last defined instance of a method.

class virtual namedDave = {
  as _;
  pri getName = "Dave";
  pub virtual count: int;
};
class dog (name) = {
  as self;
  inherit (class namedDave);
  inherit! (class pet)(name);
  pub talk = Js.log(self#getName ++ ", says Woof!");
  pub count = 0;
};
class otherDog (name) = {
  as self;
  inherit (class pet)(name);
  inherit! (class namedDave);
  pub talk = Js.log(self#getName ++ ", says Woof!");
  pub count = 1;
};
let dog1 = new dog("Not Dave");
let dog2 = new otherDog("Will be called Dave");
No diamond issues here!

Next up in this little series about objects in Reason: immutable updates.