Lily provides classes to group together a set of data and related functions. Classes are limited to single inheritance (only one parent) to keep them simple.

Declaration

class Point(x: Integer, y: Integer)
{
    public var @x = x
    public var @y = y

    public define increase(x_value: Integer, y_value: Integer)
    {
        @x += x_value
        @y += y_value
    }
}

The body of a class is where class variables are declared and is also the constructor.

Inside of a class, properties require the @ prefix, but methods do not. Outside of a class, both methods and properties are accessed through a dot. Because the dot provides dual access, methods and properties must be unique to each other.

A class method or property must always start with a scope that is either private, protected, or public.

Scope

Every class property and method must have a scope. Three scoped are supported.

public fields are available outside of the class, in a parent class, and the class itself.

protected fields are available in a parent class, and the class itself.

private fields are only available in the class itself.

Shorthand

In the above example, constructor parameters are turned directly into class properties. In such cases, the following shorthand is available.

class Point(public var @x: Integer,
            public var @y: Integer)
{
    public define increase(x_value: Integer, y_value: Integer)
    {
        @x += x_value
        @y += y_value
    }
}

Inheritance

Classes support single inheritance, denoted by <. A user-defined class can inherit from any native class. The only predefined classes that are native classes are Exception and classes that inherit from it.

class Point2D(public var @x: Integer,
              public var @y: Integer)
{
    public define increase_xy(x_move: Integer,
                              y_move: Integer)
    {
        @x += x_move
        @y += y_move
    }
}

class Point3D(x: Integer,
              y: Integer,
              public var @z: Integer) < Point2D(x, y)
{
    public define increase_xyz(x_move: Integer,
                               y_move: Integer,
                               z_move: Integer)
    {
        increase_xy(x_move, y_move)
        @z += z_move
    }
}

var p3 = Point3D(10, 200, 3000)

p3.increase_xyz(1, 2, 3)

print(p3.x) # 11
print(p3.y) # 202
print(p3.z) # 3003

Chaining

Suppose a base class has a method that would be useful in a chain with a parent class method. Class methods can return self to allow that.

class Launcher
{
    protected var @values: List[Integer] = []
    private var @error = false

    public define add_value(v: String): self
    {
        if @error: {
            return self
        }

        with v.parse_i() as Some(s): {
            @values.push(s)
        else:
            @error = true
        }
    }
}

class Runner < Launcher
{
    public define get_values: List[Integer]
    {
        return @values
    }
}

var r = Runner()
var result = r.add_value("1")
              .add_value("2")
              .get_values()

print(result) # [1, 2]

Static

Class methods receive an implicit self as their first parameter. If the static qualifier is used, no self will be sent. static must always be used after the scope.

class Utils
{
    #[
        # Invalid: static before scope.

        static public define invalid
        {
            return x * x
        }
    ]#
    public static define square(x: Integer): Integer
    {
        return x * x
    }
}

var v = Utils.square(10)

print(v) # 100

Forward

Classes are allowed to forward declare methods. While a forward definition is open, parameters cannot be defined.

class Example
{
    public var @value = 1

    forward protected define second(Integer) { ... }

    public define first(x: Integer)
    {
        if x == 0: {
            return
        else:
            @value *= 2
            second(x - 1)
        }
    }

    protected define second(x: Integer)
    {
        if x == 0: {
            return
        else:
            @value *= 2
            second(x - 1)
        }
    }
}

var v = Example()

v.first(5)

print(v.value) # 32

It is also possible to forward declare a class. There are no restrictions on declarations while there are unresolved forward classes.

forward class Entry { ... }

class EntryPool
{
    public var @entries: List[Entry] = []

    public define register(e: Entry)
    {
        @entries.push(e)
    }
}

var p = EntryPool()

class Entry
{
    public var @values: List[Integer] = []

    public define add_value(v: Integer): self
    {
        @values.push(v)
    }

    public define finish
    {
        p.register(self)
    }
}

# In another file, after importing EntryPool.
var e1 = Entry()
            .add_value(1)
            .add_value(10)
            .add_value(100)
            .finish()

var e2 = Entry()
            .add_value(1000)
            .add_value(10000)
            .add_value(100000)
            .finish()

var v = p.entries
         .map(|m| m.values.fold(0, (|a, b| a + b )))
         .fold(0, (|a, b| a + b ))

print(v) # 111111