Mixins - HeladoDeBrownie/Nexus GitHub Wiki

Overview

Mixins are the object model that Nexus uses, especially for its UI.

The Mixin library provides functions for creating and using mixins, which are in effect (dynamic) types, or classes sans (classical) inheritance. These functions are, primarily, mix and augment, which are globally available.

Making a mixin

Any mixin can be created from multiple other mixins, e.g. mix{Widget, Scalable}. This allows for a sort of multiple inheritance, although this does not handle any of the issues caused by normal multiple inheritance because the internals work differently.

To create an “abstract” mixin meant primarily to be used in other mixins, use mix{Parent1, Parent2, ...}.

To create a “concrete” mixin meant to be directly instantiated, use augment on the mixin created by mix. This adds a new method, new, that will look for initialize defined on the object.

Instantiating a mixin

The standard way of instantiating a (concrete) mixin is to call Type:new(). Similar to in Python, calling this method will call the initialize method, the only method the mixin library explicitly looks for by name.

Public and private access

Compared to using Lua tables without special metamethods, mixins allow for a form of encapsulation -- any fields on the object that are set or manipulated by the table's methods are privately scoped, and cannot be accessed without calling them.

Note that setting a field on a public instance does not allow you to alter an existing field by the same name on the private instance. Instead, the private field will shadow the public field.

local Example= augment(mix{Widget})

function Example:initialize(value)
    self.value = value
end

function Example:get_value()
   return self.value
end

local ex = Example:new("foo")
ex.value = "bar"
print(ex:get_value()) -- outputs 'foo'
print(ex.value)       -- outputs 'bar'

Inheritance

Mixins act as a sort of Inheritance -- however, note that this doesn't really work like traditional OOP inheritance.

When a mixin is created, each field (function or not) is copied from the object, so further modifications to the original mixins will not be seen. However, since mixin instances (prototypically) inherit from the mixins they were created from, modifications to that mixin will be seen from the instance.

There is no concept of “supermixins” (in the vein of superclasses in classical inheritance), and thus no super or any similar method for accessing the parent mixins. If you wish to both override a method and call what it's replacing, you must explicitly call the original mixin's method and pass the arguments yourself.

For an example, see this snippet from Modules/UI/Widget.lua, where Widget overrides the initialization method from its parent Bindable, but still calls it, explicitly passing the self parameter. If an initialization method require other values, those will need to be passed as well.

local Widget = mix{Bindable}

function Widget:initialize(color_scheme)
    Bindable.initialize(self)
    self.color_scheme = color_scheme
    self.shader = love.graphics.newShader'palette_swap.glsl'
    self.parent = nil
    self.width, self.height = 1, 1
end

Putting it all together

The following is a general purpose recipe for making a mixin, with various strategies below.

-- importing any parents and dependencies
local Parent1 = require'Parent1'
local Parent2 = require'Parent2'

local MyType = augment(mix{Parent1, Parent2})

function MyType:intialize(foo)
    -- perform any initialization here
    -- set fields by assigning them to self
    self.foo = foo
end

function MyType:get_foo()
    return self.foo
end

function MyType:thing()
    -- do something here
end

function MyType:super_method(x)
    -- equivalent to calling `super.super_method(x)` in some other language
    Parent1.method(self, x)
end

return MyType

Internals

[To be written: Details about how Mixin is implemented.]