Mixins in Lua With Various Libraries

Lua has no inherent, explicit mechanism for object-oriented programming (OOP). No class keyword or special object type, nothing of the sort. But it is possible to support OOP with varying degrees of complexity, demonstrated by Lua users.

Today I want to show you how to write and use mixins using a variety of object-oriented Lua libraries. I will assume you are familiar with the concept of mixins, and composition over inheritance, but I am not going to get into the pros and cons. However, I will implement the same logic in each library to help you compare and contrast them for the purpose of mixins. There is not going to be much commentary for each implementation as I (hopefully) will keep the code simple enough to speak for itself. And from there you can draw your own conclusion about which you prefer.

Our Example

We are writing an adventure game. Everything in the game will be an object of the Entity class, or more likely one of its sub-classes, such as Player or Enemy or Item. All entities will have X-Y coordinates. We want players and enemies to have hit-points, and we want players and items to be able to cast magic. And we want to implement this without any additional classes in our inheritance tree, which is why we are going to use mixins.

So let’s begin!

Middleclass

local class = require("middleclass")

-- First our four classes.

local Entity = class("Entity")

function Entity:initialize(x, y)
    self.x = x or 0
    self.y = y or 0
end

local Player = class("Player", Entity)
function Player:initialize(x, y)
    Entity.initialize(self, x, y)
end

local Enemy = class("Enemy", Entity)
function Enemy:initialize(x, y)
    Entity.initialize(self, x, y)
end

local Item = class("Item", Entity)
function Item:initialize(x, y)
    Entity.initialize(self, x, y)
end

-- Now the mixins.  The first is for anything with hit-points.

local Health = {
    "hp"
}

Player:include(Health)
Enemy:include(Health)

-- Now we can write code like:

local p = Player:new()
p.hp = 20
print(p.hp)

-- Our second mixin is for anything that has a cast() method.

local Magic = {
    cast = function (self, spellName)
        print(self.class.name .. " casts " .. spellName)
    end
}

Player:include(Magic)
Item:include(Magic)

-- Example usage:

p:cast("Heal")    -- Prints "Player casts Heal"

local stone = Item:new()
stone:cast("Teleport")    -- Prints "Item casts Teleport"

Classic

local Object = require("classic")
local Entity = Object:extend()

function Entity:new(x, y)
    self.x = x or 0
    self.y = y or 0
end

local Player = Entity:extend()
function Player:new(x, y)
    Player.super.new(self,x, y)
end

local Enemy = Entity:extend()
function Enemy:new(x, y)
    Player.super.new(self,x, y)
end

local Item = Entity:extend()
function Item:new(x, y)
    Player.super.new(self, x, y)
end

-- This time we do the mixin for casting first.

local Magic = Object:extend()
function Magic:cast(spellName)
    print("Casting " .. spellName)
end

Player:implement(Magic)
Item:implement(Magic)

local p = Player()
local i = Item()
p:cast("Heal")
i:cast("Teleport")

-- Now the mixin for health.  This is a little more complex than the
-- previous example because Classic only lets in objects with methods.
-- So we have to add hit-points via a method.

local Health = Object:extend()
function Health:resetHPTo(value)
    self.hp = value or 0
end

Enemy:implement(Health)

local e = Enemy()
e:resetHPTo(100)
print(e.hp)

lupy

local class = require("lupy")

class [[Entity]]

    function __init__(self, x, y)
        self.x = x or 0
        self.y = y or 0
    end
    
_end()

-- The official examples of lupy alias 'module' to 'class' when
-- distinguishing mixins from traditional classes.
local module = class

module [[Health]]

    function resetHPTo(self, value)
        self.hp = value or 0
    end
    
_end()

module [[Magic]]

    function cast(self, spellName)
        print("Casting " .. spellName)
    end
    
_end()

class [[Player < Entity]]
    include(Health)
    include(Magic)
_end()

class [[Enemy < Entity]]
    include(Health)
_end()

class [[Item < Entity]]
    include(Magic)
_end()

local p = Player()
p.resetHPTo(20)
print(p.hp)

local i = Item()
i.cast("Teleport")

30log

local class = require("30log")
local Entity = class("Entity")

function Entity:init(x, y)
    self.x = x or 0
    self.y = y or 0
end

local Player = Entity:extend("Player")
local Enemy  = Entity:extend("Enemy")
local Item   = Entity:extend("Item")

local Health = {
    resetHPTo = function (self, value)
        self.hp = value or 0
    end
}

local Magic = {
    cast = function (self, spellName)
        print(self.name .. " casts " .. spellName)
    end
}

Player:include(Health)
Player:include(Magic)
Enemy:include(Health)
Item:include(Magic)

local p = Player()
p:resetHPTo(100)
print(p.hp)
p:cast("Heal")

local i = Item()
i:cast("Teleport")

Conclusion

Middleclass is the only library which allows mixins to add properties directly to a class. Every other requires us to write a method, but as you can see this is not an impediment. Some of you may even find it preferable, which is the whole point of this article, to give you some taste of basic mixin usage in each library.

This is not an exhaustive analysis of object-oriented Lua libraries, only though which I have come across most frequently. All of this code was run with Lua 5.3 and produced the expected output. But if I have made any mistake, like failing to take advantage of some aspect of a library, then please don’t hesitate to call me out in the comments.

Advertisements

Add Your Thoughts

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s