27. Simple OOP with metatables — Homework solutions

The .lua solution files are in exercises/27/homework/solutions/.

Problem 1 — Point class with move

Problem. A Point class with .new, :distance, and :move.

How to think about it. Start from the chapter's Point class. Add one method, :move(dx, dy), that mutates self.x and self.y.

Worked solution.

local Point = {}
Point.__index = Point

function Point.new(x, y)
    local self = setmetatable({}, Point)
    self.x = x
    self.y = y
    return self
end

function Point:distance(other)
    local dx = self.x - other.x
    local dy = self.y - other.y
    return math.sqrt(dx * dx + dy * dy)
end

function Point:move(dx, dy)
    self.x = self.x + dx
    self.y = self.y + dy
end

local a = Point.new(0, 0)
local b = Point.new(3, 4)
print(a:distance(b))   -- 5.0

a:move(1, 1)
print(a.x, a.y)        -- 1   1
print(a:distance(b))   -- sqrt(2^2 + 3^2) ~ 3.6055...

Common mistakes.

  • Forgetting Point.__index = Point. Without it, a:distance(b) can't find the method, and Lua reports attempt to call a nil value.

Problem 2 — Character class

Worked solution.

local Character = {}
Character.__index = Character

function Character.new(name, hp)
    local self = setmetatable({}, Character)
    self.name = name
    self.hp = hp
    self.max_hp = hp
    return self
end

function Character:takeDamage(amount)
    self.hp = self.hp - amount
    if self.hp < 0 then self.hp = 0 end
end

function Character:heal(amount)
    self.hp = self.hp + amount
    if self.hp > self.max_hp then self.hp = self.max_hp end
end

function Character:isAlive()
    return self.hp > 0
end

function Character:report()
    print(string.format("%s: %d / %d HP (alive: %s)",
        self.name, self.hp, self.max_hp, tostring(self:isAlive())))
end

local c = Character.new("Keiko", 100)
c:report()
c:takeDamage(30)
c:report()
c:takeDamage(80)
c:report()
c:heal(20)
c:report()

A sample run:

Keiko: 100 / 100 HP (alive: true)
Keiko: 70 / 100 HP (alive: true)
Keiko: 0 / 100 HP (alive: false)
Keiko: 20 / 100 HP (alive: true)

Character:report is a good place for string.format — three values in a fixed template line.

Common mistakes.

  • Letting hp go negative or above max_hp. The two if checks in takeDamage and heal clamp the values — the clamp function from chapter 21's homework, applied as a method.

Problem 3 — Rectangle class

Worked solution.

local Rectangle = {}
Rectangle.__index = Rectangle

function Rectangle.new(w, h)
    local self = setmetatable({}, Rectangle)
    self.w = w
    self.h = h
    return self
end

function Rectangle:area()
    return self.w * self.h
end

function Rectangle:perimeter()
    return 2 * (self.w + self.h)
end

local r1 = Rectangle.new(3, 4)
local r2 = Rectangle.new(10, 2)

print(r1:area())        -- 12
print(r1:perimeter())   -- 14
print(r2:area())        -- 20
print(r2:perimeter())   -- 24

Common mistakes.

  • Storing width and height as method-level locals instead of self.w and self.h. An object exists so its fields stay with the instance.

Challenge — Animal, Dog, Cat

Worked solution.

-- Base class
local Animal = {}
Animal.__index = Animal

function Animal.new(name)
    local self = setmetatable({}, Animal)
    self.name = name
    return self
end

function Animal:describe()
    print("I am " .. self.name .. ".")
end

-- Dog inherits Animal
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog

function Dog.new(name)
    local self = Animal.new(name)
    return setmetatable(self, Dog)
end

function Dog:bark()
    print(self.name .. ": Woof!")
end

-- Cat inherits Animal
local Cat = setmetatable({}, { __index = Animal })
Cat.__index = Cat

function Cat.new(name)
    local self = Animal.new(name)
    return setmetatable(self, Cat)
end

function Cat:meow()
    print(self.name .. ": Meow.")
end

local rex = Dog.new("Rex")
local whiskers = Cat.new("Whiskers")

rex:describe()       -- I am Rex.
whiskers:describe()  -- I am Whiskers.

rex:bark()           -- Rex: Woof!
whiskers:meow()      -- Whiskers: Meow.

Dog.new and Cat.new build an Animal instance, then re-set its metatable to the more specific class. The lookup chain is now instance -> Dog -> Animal (or instance -> Cat -> Animal).

Common mistakes.

  • Forgetting Dog.__index = Dog. Then methods on Dog aren't reachable from instances; lookups skip straight to Animal, because that is what the metatable of Dog (not Dog itself) points at. Two __index lines, on two different objects, doing two different jobs.

Done?

Metatable-based classes (Point, Character, Rectangle) and a small inheritance chain (Animal -> Dog/Cat) are now in your toolkit. The next three chapters build on this: Many objects together manages collections of instances, Inheritance in depth specialises one class from another, and Designing a small class makes a class pleasant to use.