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
hpgo negative or abovemax_hp. The twoifchecks intakeDamageandhealclamp the values — theclampfunction 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()) -- 24Common mistakes.
- Storing
widthandheightas method-level locals instead ofself.wandself.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 onDogaren't reachable from instances; lookups skip straight toAnimal, because that is what the metatable ofDog(notDogitself) points at. Two__indexlines, 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.