Slither

Slither

Slither is a class library for lua heavily inspired by the python programming language.

Defining a class

The basic syntax used for defining a class is as follows:

class "ClassName"
{
    member = 3,

    method = function(self)
        print(self.member)
    end,

    classMethod = function()
        print("Class member!")
    end,
}

instance = ClassName()

This creates a class named ClassName, and returns the newly created class. The prototype for this class is the table constructed (so whatever is between the curly braces). The last line creates an instance of the class, named instance.

In this case, an object contains a field member with value 3, a method method that prints said value, and a method on the class classMethod. Note that whilst each instance has its own members, when instantiating they are referenced to each other. If member was a table, setting self.member is not shared, but setting self.member.something is shared. When working with tables, it's often useful to create them in the constructor (__init__) instead.

Anonymous classes and return values

In old versions of slither, it would set a global variable with the name passed as class name. Since setting globals isn't good behaviour, modern versions of slither return the class instead. This means you'll often write code like:

local ClassName = class "ClassName"
{
}

Of course the names do not need to match (for better or for worse), but generally this means repeating the class name. For debugging reasons (see __name__) it is often useful to specify a name, and it's therefore recommended to do so. If no name is specified however, slither can generate a name instead. These names are not designed to be identifiable, simply unique. Syntactically, defining an anonymous class is as simple as not specifying the class name, the rest of the syntax remains the same. An equivalent definition, albeit without the name, would be:

local ClassName = class
{
}

Inheritance

If we've first defined a parent class as follows:

ParentClass = class "ParentClass"
{
    member = 3,

    method = function(self)
        print(self.member)
    end,
}

We can then inherit from it using:

class "Subclass" (ParentClass)
{
    member = 5,
}

Subclass will have inherited any (class/object) members (including methods) from ParentClass, overriding anything it contains itself (member in this case.) If no parent class is specified, class.Object is automatically selected. This means that every class always derives from class.Object (except class.Object itself).

Slither also supports multiple inheritance, using a well-defined lookup order. This member resolution order (MRO), is much like in python. Its behaviour is best described using an example:

O  O  O  O  O
|  |  |  |  |
A  B  C  D  A
\  |  /  \ /
   E      F
   \      /
    \    /
     \  /
       G

The MRO is now from left to right, depth first, but for duplicate entries, the last option is chosen. The left-to-right depth-first order is G, E, A, O, B, O, C, O, F, D, O, A, O. Removing all but the last occurrence of each entry, a lookup in G will search the parents in the following order: G, E, B, C, F, D, A, O. This means that in this example, both E and F can override methods in A, and G will prefer E's overrides over F's.

Helper functions

Slither also defines the functions class.issubclass and class.isinstance, which determine recursively whether a class is derived from another, or an object is an instance of a class, or its subclasses, respectively.

class.issubclass allows for the following invocations:

boolean = class.issubclass(class, parent)
boolean = class.issubclass(class, {parents...})

isinstance allows for the following invocation:

boolean = class.isinstance(object, parent)
boolean = class.isinstance(object, {parents...})

In general, isinstance is equivalent to calling issubclass with the same arguments, substituting the object for its class.

Slither also provides class.super, which can be used to look up the next implementation of a method in the MRO.

value = class.super(class, instance, key)

Note that instance can also be a class itself, but it should always be the class or object this method was called on, rather than the class which defines this method. In other words it represents (an object of) the most specific subclass. An example use of class.super is as follows:

BaseClass = class "BaseClass"
{
    method = function(self)
        print("BaseClass implementation")
    end,
}

DerivedClass = class "DerivedClass" (BaseClass)
{
    method = function(self)
        print("DerivedClass implementation")
        class.super(DerivedClass, self, "method")(self)
    end,
}

Special methods and members

Predefined members

  • __class__: Returns the class this object is an instance of.
  • __name__: Returns the name of this object's class.
  • __mro__: The MRO of this class, as a flat table.
  • __parents__: A map of classes to booleans, true for all (recursive) parents of this class.
  • __subclasses__: A map of classes to booleans, true for all (recursive) children of this class. Note that this value is usually used on the class, but is available on instances as well.
  • __prototype__: The original table passed to the class constructor.

Operator overloading

Slither allows operator overloading, and it does this using the following methods:

  • __new__: Overrides construction of this class: gets the class as argument, needs to return an instance. Slither's class.Object provides a default __new__.
  • __init__: Called when an object is instantiated. Receives arguments passed when instantiating, returns nothing.
  • __cmp__: Overrides most comparison operators, gets called with two arguments, return 0 when they are equal, a negative number if a is less than b, and a positive number otherwise.
  • __call__: Gets called when the object is called like a function (obj()).
  • __len__: Override the length returned using the length (#) operator.
  • __add__: Overrides addition.
  • __sub__: Overrides subtraction.
  • __mul__: Overrides multiplication.
  • __div__: Overrides division.
  • __mod__: Overrides the modulo operation.
  • __pow__: Overrides the power (^) operator.
  • __neg__: Overrides the unary minus (or negation).
  • __getattr__: Overrides the return value of an undefined index operation.
  • __setattr__: Overrides the result of setting an undefined member.

Attributes

  • __attributes__: A list of callables that are called on the created class object (not instances), allowing for modification and replacement. Not inherited.

Annotations

Annotations are the more powerful cousin of attributes. Whereas attributes are simply functions that take a class as parameter, annotations can modify classes, members and methods during instantiation. All annotations are instances of (subclasses of) the slither-provided class.Annotation class. An annotation can be applied either to a member (both values and methods) by using +, or to a class by using - (see below). When applied to a member, the method apply is called with the current value, the name of the member and the class prototype, the return value replaces the member in the class. If apply returns an additional value, this is stored in the class' metadata for later retrieval. When applied to a class itself, applyClassPre before member annotations are called with the class name and its prototype. This is useful when adding members that may themselves have annotations, or when you need to operate on the prototype itself. After the class has been constructed, applyClassPost gets called with the class name and the now-constructed class.

Additional helpers are provided as class methods:

  • annotation:get(target, name): Gets the metadata associated with annotation class annotation and member name in class target.
  • annotation:iterate(target): Iterate over all metadata associated with annotation class annotation in class target. Returns pairs of member name and metadata.
  • annotation:iterateFull(target): Like annotation:iterate(target), but also returns metadata associated with subclasses of annotation.

Slither comes with one inbuilt annotation: class.Override. When class.Override is applied to a member, it checks whether a member with the same name exists in one of the parent classes.

An example annotation could be defined and used as follows:

ExampleAnnotation = class "ExampleAnnotation" (class.Annotation)
{
    apply = function(self, value, name, prototype)
        print(("Defining member %q: %s"):format(name, tostring(value)))
        return f
    end,

    applyClassPre = function(self, name, prototype)
        print(("Defining class %q"):format(name))
    end,

    applyClassPost = function(self, name, class)
        print(("Instantiated class %q"):format(name))
    end,
}

class "Example"
{
    -ExampleAnnotation(),

    value = ExampleAnnotation() + 5,

    method = ExampleAnnotation() + function(self)
        return 5
    end,
}

Miscellaneous features

This library has support for Class Commons, a "standard" for lua class libraries that allows libraries to target multiple class libraries, and provide native classes for their users.

It is explicitly supported by gvx' serialisation libraries bitser and Lady. Let me know if your libraries support slither too!

Allocate

Sometimes, especially when serialising, you'll find yourself wanting to construct a class without calling the constructor. Depending on your particular usage, you may either want to call the __new__ class method or the allocate metamethod. The difference between the two is whether any custom user code is run.

The allocate metamethod on a class simply blesses (and optionally creates) a table into a class instance. If an argument is passed, the passed table is blessed, otherwise a new one is constructed. The resulting class instance is always returned.

The __new__ class method as defined in class.Object uses allocate internally to create a new class instance. Further overrides in subclasses can add additional behaviour, like adding members, or registering the newly created object.

Generally the best solution is to call __new__, as it constructs objects more faithfully (but does not call the constructor). If no user code is to run at all allocate is the method of choice. Though slither cannot distinguish these instances from "real" instances, if the user code expects the constructor to run and you do not call it manually, these instances may not be equivalent to "real" instances. If instances are to be created using these methods, for instance when deserialising, it is probably wise to warn the user in documentation.