A Tour of Morfa

Basic usage of aggregate types

Structs

Let's start with an example struct for representing complex numbers:

import morfa.math.base: sqrt;

struct Complex
{
    var re: float;
    var im: float;

    func magnitude(): float
    {
        return sqrt(re * re + im * im);
    }

    static func conjugate(c: Complex): Complex
    {
        return Complex(c.re, -c.im);
    }
}

Complex may be used as follows:

unittest
{
    // Create a Complex and initialize its fields
    var c = Complex(3.0, 4.0); 
    assert(c.re == 3.0);
    assert(c.im == 4.0);

    // Call a Complex's method
    var mag = c.magnitude();   
    assert(mag == 5.0);
}

The expression Complex(3.0, 4.0) is a struct literal. It's value is a Complex with the fields re and im initialized to 3.0 and 4.0, respectively.

If you declare a Complex variable but do not provide an initializer, re and im will be initialized with default values for float:

import morfa.math.base;

unittest
{
    var complexNaN: Complex;
    assert(isNaN(complexNaN.re) and isNaN(complexNaN.im));
}

Also note the static member of Complex. As in C++ or Java, static member functions of aggregate types are not associated with any object of the type.

unittest
{
    var c = Complex(1.0, 2.0);
    var c2 = Complex.conjugate(c);
    assert(c2.re == c.re and c2.im == -c.im);
}

Values of struct types are similar to values of primitive types like int and float in that they are passed to—and returned from—functions by value. Consider the following definition of +:

func + (a: Complex, b: Complex): Complex
{
    a.re += b.re;
    a.im += b.im;
    return a;
}

In the body of +, a is the copy of the call argument and hence changes to a do not affect the argument:

unittest
{
    var a = Complex(2.0, 1.0);
    var b = Complex(1.0, -2.0);
    var c = a + b;

    assert(c == Complex(3.0, -1.0));
    // a does not change
    assert(a == Complex(2.0, 1.0));
}

Our second example makes more use of methods that change the state of a struct value.

struct Counter
{
    var value: int;

    property count(): int
    {
        return value;
    }

    func incr(): void
    {
        value += 1; 
    }

    func reset(): void
    {
        value = 0;
    }
}

unittest
{
    var c: Counter;
    assert(c.count == 0);
    c.incr();
    c.incr();
    assert(c.count == 2);
    c.reset();
    assert(c.count == 0);
}

Classes

A class for implementing linked lists may be defined as follows:

class Node
{
    var data: int;
    var next: Node;

    func new(data_: int, next_: Node)
    {
        data = data_;
        next = next_;
    }

    func new(data_: int)
    {
        this.new(data_, null); // Call the other constructor
    }

    func length(): int
    {
        return if (next != null) 1 + next.length() else 1;
    }

    func last(): Node
    {
        return if (next == null) this else next.last();
    }

    func append(list: Node): void
    {
        last().next = list;
    }
}

Functions called new are constructors. The keyword new is also used to create a class instance:

unittest
{
    var list = new Node(1, new Node(2, new Node(3)));
    list.append(new Node(4));
    assert(list.length() == 4);
}

A constructor may call another one, prefixing new with this as in the second constructor of Node (or with super when calling a superclass constructor, see Section Inheritance).

Classes that have no explicit constructor are created as if they had one with no arguments:

class Empty {}

unittest
{
    var e = new Empty();
    var e2 = new Empty;  // equivalent to the above, empty parentheses are optional
}

Value semantics vs reference semantics

The difference in the syntax for creating struct and class values reflects the difference between the "value semantics" of the former vs the "reference semantics" of the latter:

  • Variables of a struct type are a "storage" for struct members while variables of a class type are "referenes" to class instances.
  • Struct values are allocated on the stack of the enclosing function while class instances are allocated on the program heap.
  • The default value of a struct type is a struct value with members initialized to default values of their respective types, while the default value of a class type is the null reference.

More on classes and structs

The following sections may safely be skipped on first reading.

Access modifiers and nesting

Members of a class or struct may be declared public or private, private being the default. private members are inaccessible outside the module in which the aggregate type is declared. An additional modifier protected allowed only for class members will be discussed in Inheritance.

static modifier separates the field from the class/struct instance, quite similar to C++ practice:

class Countess
{
    var iCount: int;
    static var cCount: int;

    public func instanceCount(): int
    {
        return iCount;
    }

    public static func classCount(): int
    {
        // return iCount; // would not compile
        return cCount;
    }
}

abstract, final and override modifiers can be applied to classes, member functions and/or properties—this will be discussed in detail in following sections.

Since classes can be nested, public, private and static modifiers apply as expected (again, see Inheritance for anonymous classes used below):

class Outer
{
    var data: text;
    static var sData: text;

    public class NestedNonstatic
    {
        public func ownersData()
        {
            return data; // I have access to some instance of Outer
        }
    }

    public static class NestedStatic
    {
        public func ownersClassData()
        {
            // return data; // would not compile, no access to this
            return sData; // OK
        }
    }
}

unittest
{
    // var object = new Outer.NestedNonstatic(); // would not compile - no access to this
    var object = new Outer.NestedStatic(); // OK
    var iAmAThis = new class Outer
    {
        public func new()
        {
            var nestedNonstaticInstance = new NestedNonstatic(); // OK - I have a this here
        }
    };

    // to construct Outer.NestedNonstatic here we need to provide an instance of Outer explicitly
    var outer = new Outer;
    var object2 = outer.new Outer.NestedNonstatic();
}

Argument passing

As mentioned before classes are passed to and returned from functions by reference, whilst structs by value. The following should therefore be no surprise:

func decr(c: Countess)
{
    c.iCount -= 1; // will modify on the outside this function
}

func decr(c: Counter)
{
    c.value -= 1; // change will not be visible outside this function
}

unittest
{
    var cter = Counter(1);
    assert(cter.count == 1);
    decr(cter);
    assert(cter.count == 1);

    var ctess = new Countess;
    ctess.iCount = 1;
    decr(ctess);
    assert(ctess.iCount == 0);
}

Destructors and scope modifier

Classes may define destructors to provide behaviour on destruction of the object, by having a delete member function. Since Morfa is garbage collected, the moment when the destructor is called is unknown, unless scope modifier is used for a variable.

Take care not to return from a function nor otherwise extend the accessibility of an object pointed by a scope var variable.

class Destructy
{
    var data: text;

    func new()
    {
        data = "Fresh, just constructed";
    }

    func delete()
    {
        data = "GC got me. Will be destroyed and dealocated";
    }
}

unittest
{
    var willBeDeletedAsGCWishes = new Destructy;
    scope var willBeDeletedOnLeavingUnittest = new Destructy;
}