Grok Object oriented programming - vilinski/nemerle GitHub Wiki

This page is a part of the Grokking Nemerle tutorial.

Once again a definition from Wikipedia.

OOP is all about objects. Objects consist of some data and methods to operate on this data. In functional programming we have functions (algorithm) and data. The things are separate. One can think about objects as of records (structures) with attached functions.

Nemerle allows programmers to program in this style. Moreover, the class library is very deeply object oriented. Therefore OOP is unavoidable in Nemerle.

Table of Contents

Back in the Refrigerator

While talking about XML, we have shown an example of a refrigerator. It was a degenerated object -- a record. Record is a bunch of data, or an object without methods. Now we will try to extend it a bit.

class Refrigerator
{
  public mutable minimal_temperature : float;
  public mutable content : list [RefrigeratorContent];

  public AddContent (elem : RefrigeratorContent) : void
  {
    content = elem :: content
  }
}

variant RefrigeratorContent
{
  | Beer { name : string; volume : float; }
  | Chips { weight : int; }
  | Ketchup
}

Now, in addition to fields with content and temperature, the refrigerator has a method for adding new content.

The definition of method looks much like the definition of a function within a module.

It is quite important to understand the difference between classes and objects. Classes are type definitions, while objects (class instances) are values. Classes define templates to create new objects.

Non static methods defined in class C have access to a special value called this. It has type C and is immutable. It refers to the current object. You can use the dot (.) operator to access fields of current object. You can see how this is used in the AddContent method.

The this value is quite often used in object oriented programming. Therefore it can be omitted for brevity. For example:

public AddContent (elem : RefrigeratorContent) : void
{
  content = elem :: content
}

However, if the method had a formal parameter called content, or a local value with such a name, one would need to use this.content to access the field.

There is one special method in a class called a constructor. It is called whenever you request creation of new instance of an object. It the responsibility of the constructor to setup values of all fields within an object. Fields start with value of null, 0 or 0.0.

public this ()
{
  minimal_temperature = -273.15;
  content = [];
}

Constructors can take parameters. For example, if we wanted to set the minimal_temperature already at the object construction stage, we could write:

public this (temp : float)
{
  minimal_temperature = temp;
  content = [];
}

For variant options Nemerle provides a default constructor, that assigns each field. If you do not provide a constructor for a regular class, an empty one is generated. If you need the field-assigning constructor, you can use [Record] attribute, like this:

[Record]
class Foo
{
  x : int;
  y : float;
  z : string;
}

The following constructor is generated:

public this (x : int, y : float, z : string)
{
  this.x = x;
  this.y = y;
  this.z = z;
}

The constructor is called with the name of the class when creating new objects. Other methods are called using the dot operator. For example in refr.AddContent (Ketchup ()) the refr is passed to the AddContent method as the this pointer and Ketchup () is passed as elem formal parameter.

module Party {
  Main () : void
  {
    def refr = Refrigerator ();
    refr.AddContent (Beer ("Tiskie", 0.60));
    refr.AddContent (Ketchup ());
  }
}

Fortunately, objects are not just a fancy notation for a function application on records.

Inheritance

Classes can inherit from other classes. The fact that a class B inherits from a class A has a few consequences. The first one is that B has now all the fields and methods of A. The second one is that B is now subtype of A. This means that all the functions operating on A can now also operate on B.

Class A is often called parent or base class of B (which is derived class).

In the following example we can see how we can call methods defined in the base class (AddContent), as well as from the derived class (MoveToBedroom).

Static methods and the constructor are not derived. The parameterless constructor is defined in this example. As the first thing to do, it calls parameterless constructor of the base class. It does it, so the derived fields are initialized first. Then it initializes the new fields.

The call to the parameterless parent constructor is in fact redundant. When there is no call to the parent class constructor, such a parameterless parent constructor call is assumed in the first line of a constructor.

class RefrigeratorWithRolls : Refrigerator
{
  public mutable position_x : int;
  public mutable position_y : int;

  public MoveBy (dx : int, dy : int) : void
  {
    position_x += dx;
    position_y += dy;
  }

  public MoveToBedroom () : void
  {
    position_x =  42;
    position_y = -42;
  }

  public this ()
  {
    base ();

    position_x = 0;
    position_y = 0;
  }
}

class TheDayAfter
{
  static Main () : void
  {
    def refr = RefrigeratorWithRolls ();
      for (mutable i = 0; i < 10; ++i)
      refr.AddContent (Beer ("Liech", 0.5));
    refr.MoveToBedroom ();
    // drink
  }
}

Virtual calls

The funny part begins where objects can react to calling some methods in a way dependent on the class of the object. It is possible to define virtual methods, which means they can be redefined in a derived class. Then when we have a function working on the base class, whenever it calls the virtual method, an appropriate method is selected base on actual object type.

This feature is called polymorphism in object-oriented world. We will, however, mostly use this word for another kind of polymorphism -- parametric polymorphism.

When one wants to override a virtual method from a base class, it needs to be declared with the override modifier.

using Nemerle.IO;

class Refrigerator
{
  public mutable minimal_temperature : float;
  public mutable content : list [RefrigeratorContent];

  public virtual AddContent (elem : RefrigeratorContent) : void
  {
    content = elem :: content
  }
  
  public this ()
  {
    minimal_temperature = -273.15;
    content = [];
  }
}

class RestrictedRefrigerator : Refrigerator
{
  public override AddContent (elem : RefrigeratorContent) : void
  {
    match (elem) {
      | Ketchup =>
        // don't add!
        printf ("Ketchup is not healthy!\n")
      | _ =>
        content = elem :: content
    }
  }
  
  public this ()
  {
  }
}

Here we can see how the AddKetchup calls different methods depending on actual object type. The first call adds ketchup, the second call refuses to do so.

module Shop
{
  AddKetchup (refr : Refrigerator) : void
  {
    refr.AddContent (Ketchup ())
  }

  Main () : void
  {
    def r1 = Refrigerator ();
    def r2 = RestrictedRefrigerator ();
    AddKetchup (r1);
    AddKetchup (r2);
  }
}

Interfaces

The .NET Framework supports only single inheritance. This means that any given class can derive from just one base class. However, it is sometimes needed for a class to be two or more different things depending on context. .NET supports it (just like Java) through interfaces. An interface is a contract specifying a set of methods given class should implement. A class can implement zero or more interfaces (in addition to deriving from some base class).

Implementing interface implies subtyping it. That is if you have a class A implementing I and method taking I as parameter, then you can pass A as this parameter.

Interfaces most commonly state some ability of type. For example, the ability to convert itself to some other type or to compare with some other types.

using Nemerle.IO;

interface IPrintable {
  Print () : void;
}

class RefrigeratorNG : Refrigerator, IPrintable
{
  public Print () : void
  {
    printf ("I'm the refrigerator!\n")
  }

  public this ()
  {
  }
}

module RP {
  PrintTenTimes (p : IPrintable) : void
  {
    for (mutable i = 0; i < 10; ++i)
      p.Print ()
  }
  
  Main () : void
  {
    def refr = RefrigeratorNG ();
    PrintTenTimes (refr)
  }
}

The base class must come first after the colon in class definition. Then come interfaces in any order.

⚠️ **GitHub.com Fallback** ⚠️