programming/lua/classes: prose version
it's much more readable is it not
This commit is contained in:
		
							parent
							
								
									d8d3ebdd38
								
							
						
					
					
						commit
						c612929825
					
				
					 2 changed files with 468 additions and 1 deletions
				
			
		
							
								
								
									
										467
									
								
								content/programming/lua/classes.dj
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										467
									
								
								content/programming/lua/classes.dj
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,467 @@
 | 
			
		|||
title = "Classes in Lua"
 | 
			
		||||
 | 
			
		||||
+++
 | 
			
		||||
 | 
			
		||||
While reading Lua, you may have stumbled upon something that looks like this:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
-- Declare a base `Entity` class...
 | 
			
		||||
 | 
			
		||||
local Entity = Object:inherit()
 | 
			
		||||
 | 
			
		||||
function Entity:tick() end
 | 
			
		||||
function Entity:draw() end
 | 
			
		||||
 | 
			
		||||
-- and an inheriting `Player` class.
 | 
			
		||||
 | 
			
		||||
local Player = Entity:inherit()
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This is the way prople generally approach object-oriented programming in the language.
 | 
			
		||||
For someone coming from a language like Java, where classes are a syncactic construct---`class Cat extends Animal`---it can feel weird to see them handled this way---as local variables, using regular functions to implement inheritance.
 | 
			
		||||
 | 
			
		||||
But worry not!
 | 
			
		||||
This tutorial will hopefully clear up any confusion you might have, using beginner-friendly language, and simple examples.
 | 
			
		||||
 | 
			
		||||
## Metatables
 | 
			
		||||
 | 
			
		||||
Before we start, we need to talk about *metatables*.
 | 
			
		||||
These are Lua's way of allowing you to _overload operators_.
 | 
			
		||||
 | 
			
		||||
Consider an operation like `+`:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
print(1 + 2) --> 3
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The `+` operator, by default, performs arithmetic addition.
 | 
			
		||||
However, with metatables, we can _overload_ its meaning for when it's used with our own table on the left.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local v = { x = 1, y = 2 }
 | 
			
		||||
 | 
			
		||||
setmetatable(v, {
 | 
			
		||||
    __add = function (t, u)
 | 
			
		||||
        return t.x + t.y + u
 | 
			
		||||
    end,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
print(v + 3) --> 6
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Overloadable operators in Lua include not only your usual arithmetic `+`, `-`, `*`, `/`, but also things like indexing tables `a[b]`, creating new indices in tables `a[b] = c`, or function calls `a(b, c, d)`.
 | 
			
		||||
Each operator has a special name in the metatable, and each operator's name is prefixed with `__`, to signal that it's special.
 | 
			
		||||
 | 
			
		||||
### `__index`
 | 
			
		||||
 | 
			
		||||
Today, we'll be focusing on `__index`, because it's arguably the most important of them all.
 | 
			
		||||
It allows us to specify what should be done when the `a[b]` indexing operator _fails_ (is about to return `nil`.)
 | 
			
		||||
 | 
			
		||||
Consider this example.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local t = { a = 1 }
 | 
			
		||||
print(t.b) --> nil
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
In this case, `t` does not have a key `"b"`, and `t` has no metatable with `__index`, so `nil` is returned.
 | 
			
		||||
So let's try adding that `__index` function, to tell Lua what to do instead.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local fallback = { b = 2 }
 | 
			
		||||
 | 
			
		||||
setmetatable(t, {
 | 
			
		||||
    -- The first argument is the table that's indexed,
 | 
			
		||||
    -- and the second argument is the index.
 | 
			
		||||
    -- i.e. the arguments map to `the_table[index]`.
 | 
			
		||||
    __index = function (the_table, index)
 | 
			
		||||
        return fallback[index]
 | 
			
		||||
    end,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
print(t.b) --> 2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Our function is called, it looks in `fallback` to figure out what to return instead, and indeed---`2` is returned instead of `nil`!
 | 
			
		||||
 | 
			
		||||
However, `__index` is special---it does not have to be set to a function.
 | 
			
		||||
We can also set it to a table, as a shorthand for the above form.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
setmetatable(t, {
 | 
			
		||||
    __index = fallback,
 | 
			
		||||
})
 | 
			
		||||
print(t.b) --> 2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This way of doing things avoids a lot of typing, as well as an extra memory allocation coming from that local function---which can get costly if you run it many times in a game loop!
 | 
			
		||||
 | 
			
		||||
## Method call syntax
 | 
			
		||||
 | 
			
		||||
There is one thing we need to get out of the way before we move on, and that is Lua's _method call syntax_ `a:method(b)`.
 | 
			
		||||
 | 
			
		||||
This syntax is equivalent to the following.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
a.method(a, b)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Basically, the thing before the colon `:` is passed as the first argument to the thing before `:`'s `method` function.
 | 
			
		||||
 | 
			
		||||
Lua also has a syntax sugar for declaring functions on tables:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local t = {}
 | 
			
		||||
 | 
			
		||||
function t.do_stuff()
 | 
			
		||||
    print("hi")
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- equivalent to:
 | 
			
		||||
 | 
			
		||||
t.do_stuff = function ()
 | 
			
		||||
    print("hi")
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
So to complement the `:` method call syntax, there's also the `:` function declaration syntax, which inserts a `self` parameter before all the other ones.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
function t:do_thing()
 | 
			
		||||
    self.aaa = 1
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- equivalent to:
 | 
			
		||||
 | 
			
		||||
function t.do_thing(self)
 | 
			
		||||
    self.aaa = 1
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The call and declaration syntaxes are not tied together in any way, so you can call `:`-defined functions with `.` and vice versa, but it's probably better not to.
 | 
			
		||||
Bear in mind that your function definitions also serve the purpose of documentation, and using the `:` syntax in definitions suggests that the way your function is supposed to be called is through the `:` operator.
 | 
			
		||||
 | 
			
		||||
With that knowledge, we can more on to modelling classes.
 | 
			
		||||
 | 
			
		||||
## Classes
 | 
			
		||||
 | 
			
		||||
We can use the `__index` fallback operator to model classes quite easily.
 | 
			
		||||
Let's create a class `Cat`, with two functions `meow` and `feed`.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local Cat = {}
 | 
			
		||||
 | 
			
		||||
function Cat:meow()
 | 
			
		||||
    print("meow")
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function Cat:feed()
 | 
			
		||||
    self.food = self.food + 1
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
We will also need a function for creating cats, which we'll name `new`.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
function Cat:new()
 | 
			
		||||
    local cat = {}
 | 
			
		||||
    cat.food = 10
 | 
			
		||||
    return cat
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
We can now use the API like so:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local kitty = Cat:new()
 | 
			
		||||
Cat.meow(kitty)
 | 
			
		||||
Cat.feed(kitty)
 | 
			
		||||
print(kitty.food) --> 11
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
However, note how we have to namespace the `Cat` functions explicitly, and we cannot use the `:` method call operator yet.
 | 
			
		||||
The table returned by `Cat:new()` does not have the functions `meow` and `feed` for that to work.
 | 
			
		||||
 | 
			
		||||
So to provide it with these functions, we can use our handy `__index` metamethod.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
function Cat:new()
 | 
			
		||||
    local cat = {}
 | 
			
		||||
    cat.food = 10
 | 
			
		||||
    -- setmetatable returns its first argument. How convenient!
 | 
			
		||||
    return setmetatable(cat, { __index = Cat })
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Now, we're able to create cats that can meow on their own.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
kitty = Cat:new()
 | 
			
		||||
kitty:meow()
 | 
			
		||||
kitty:feed()
 | 
			
		||||
print(kitty.food) --> 11
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
However, creating an extra metatable every single time we create a cat is pretty inefficient!
 | 
			
		||||
We can exploit the fact that Lua doesn't really care about metatable fields it doesn't know about, and make `Cat` itself into a metatable.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
Cat.__index = Cat
 | 
			
		||||
 | 
			
		||||
function Cat:new()
 | 
			
		||||
    local cat = {}
 | 
			
		||||
    cat.food = 10
 | 
			
		||||
    return setmetatable(cat, Cat)
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
But note how we've declared `Cat:new` with the special method syntax.
 | 
			
		||||
We call the function like `Cat:new()`, which is equivalent to `Cat.new(Cat)`, which means that the implicit `self` parameter _is_ the `Cat` table already!
 | 
			
		||||
Thus, we can simplify the call to `setmetatable`, to remove the redundant reference to `Cat`.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
    return setmetatable(cat, self)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
With all these improvements, here's how the code looks so far.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local Cat = {}
 | 
			
		||||
Cat.__index = Cat
 | 
			
		||||
 | 
			
		||||
function Cat:new()
 | 
			
		||||
    local cat = {}
 | 
			
		||||
    cat.food = 10
 | 
			
		||||
    return setmetatable(cat, self)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function Cat:meow()
 | 
			
		||||
    print("meow!")
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function Cat:feed()
 | 
			
		||||
    self.food = self.food + 1
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Inheritance
 | 
			
		||||
 | 
			
		||||
Given this fairly simple way of creating classes, we can now expand this idea to inheritance.
 | 
			
		||||
 | 
			
		||||
Conceptually, inheriting froma class is pretty straightforward: what we want to do, is to have all of the parent class's methods available on the child class.
 | 
			
		||||
I think you might see where this is going now: all we need to do to create a subclass, is to create a new class, whose metatable's `__index` points to the parent class.
 | 
			
		||||
 | 
			
		||||
Let's rewrite our example with the kitty to generalise animals under a single class.
 | 
			
		||||
 | 
			
		||||
- class `Animal`, abstract
 | 
			
		||||
 | 
			
		||||
    - variable `food`: integer
 | 
			
		||||
    - function `speak()`
 | 
			
		||||
    - function `feed()`
 | 
			
		||||
 | 
			
		||||
- class `Cat`, extends `Animal`
 | 
			
		||||
 | 
			
		||||
    - function `speak()`
 | 
			
		||||
 | 
			
		||||
Starting with the base `Animal` class...
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local Animal = {}
 | 
			
		||||
Animal.__index = Animal
 | 
			
		||||
 | 
			
		||||
-- We don't create a `new` method, because we don't want people
 | 
			
		||||
-- creating "generic" animals. This makes our class _abstract_.
 | 
			
		||||
 | 
			
		||||
-- speak() is a function that must be overridden by all subclasses,
 | 
			
		||||
-- so we make it error by default when called.
 | 
			
		||||
function Animal:speak() error("not implemented") end
 | 
			
		||||
 | 
			
		||||
function Animal:feed()
 | 
			
		||||
    self.food = self.food + 1
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
We can define `Cat` to be a subclass of `Animal`, and have it inherit `Animal`'s keys, by using `__index`.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local Cat = {}
 | 
			
		||||
 | 
			
		||||
-- We still need to override __index, so that the metatable
 | 
			
		||||
-- we set in our own constructor has our overridden `speak()` method.
 | 
			
		||||
Cat.__index = Cat
 | 
			
		||||
 | 
			
		||||
-- To be able to call `Animal` methods from `Cat`, we set it
 | 
			
		||||
-- as its metatable. Remember that `Animal.__index == Animal`.
 | 
			
		||||
setmetatable(Cat, Animal)
 | 
			
		||||
 | 
			
		||||
function Cat:new()
 | 
			
		||||
    -- Ultra-shorthand way of initializing a class instance!
 | 
			
		||||
    -- No need to declare any temporary locals, we can pass
 | 
			
		||||
    -- the table into `setmetatable` right away, and it will
 | 
			
		||||
    -- return back the table we passed to it.
 | 
			
		||||
    return setmetatable({
 | 
			
		||||
        food = 1,
 | 
			
		||||
    }, self)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Don't forget to override speak(), otherwise calling it
 | 
			
		||||
-- will error out!
 | 
			
		||||
function Cat:speak()
 | 
			
		||||
    print("meow")
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Note now how declaring `speak` _does not modify `Animal`_.
 | 
			
		||||
For that, we would need to set the `__newindex` metamethod on the `Animal`, not just `__index`.
 | 
			
		||||
 | 
			
		||||
Now we can create instances of `Cat`, and it will inherit the `feed` method from `Animal`.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local kitty = Cat:new()
 | 
			
		||||
kitty:speak()
 | 
			
		||||
kitty:feed()      -- inherited!
 | 
			
		||||
print(kitty.food) --> 2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Packing it up into a nice box
 | 
			
		||||
 | 
			
		||||
With all this, we are now ready to pack this subclassing functionality into a nicer package.
 | 
			
		||||
Speaking of package, let's create a module `class.lua`.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local Class = {}
 | 
			
		||||
Class.__index = Class
 | 
			
		||||
 | 
			
		||||
return Class
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Now, let's create a function for inheriting from the class.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
-- insert above `return Class`
 | 
			
		||||
 | 
			
		||||
function Class:inherit()
 | 
			
		||||
    local Subclass = {}
 | 
			
		||||
    Subclass.__index = Subclass
 | 
			
		||||
    -- Note how `self` in this instance is the parent class,
 | 
			
		||||
    -- as we call the function like `SomeClass:inherit()`.
 | 
			
		||||
    setmetatable(Subclass, self)
 | 
			
		||||
    return subclass
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This is going to let us cleanly inherit from classes, without needing to copy and paste all the `__index` and `setmetatable` boilerplate into all subclasses.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local Class = require "class"
 | 
			
		||||
local Sub = Class:inherit()
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The other boilerplatey bit was initialisation, so let's take care of that.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
-- insert below the `end` of `function Class:inherit()`
 | 
			
		||||
 | 
			
		||||
-- By default, let's make the base `Class` impossible to instantiate.
 | 
			
		||||
-- This should catch bugs if a subclass forgets to override `initialize`.
 | 
			
		||||
function Class:initialize()
 | 
			
		||||
    error("this class cannot be initialized")
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- `...` is Lua's notation for collecting a variable number of arguments
 | 
			
		||||
function Class:new(...)
 | 
			
		||||
    local instance = {}
 | 
			
		||||
    -- `self` is the class we're instantiating, as this function
 | 
			
		||||
    -- is called like `MyClass:new()`
 | 
			
		||||
    setmetatable(instance, self)
 | 
			
		||||
    -- We pass the instance to the class's `initialize()` method,
 | 
			
		||||
    -- along with all the arguments we received in `new()`.
 | 
			
		||||
    self.initialize(instance, ...)
 | 
			
		||||
    return instance
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Having that, we can now rewrite our `Animal` example to use our super simple class library.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local Class = require "class"
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
local Animal = Class:inherit()
 | 
			
		||||
 | 
			
		||||
-- We'll provide a convenience function for implementers,
 | 
			
		||||
-- for initialising the food value, as well as any other
 | 
			
		||||
-- base fields that may come up.
 | 
			
		||||
function Animal:_initialize()
 | 
			
		||||
    self.food = 1
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- However, we do not want to override initialize(), as
 | 
			
		||||
-- that would make our class concrete rather than abstract!
 | 
			
		||||
-- Remember that we don't want to make it possible to create
 | 
			
		||||
-- Animal instances on their own.
 | 
			
		||||
 | 
			
		||||
function Animal:speak()
 | 
			
		||||
    error("unimplemented")
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function Animal:feed()
 | 
			
		||||
    self.food = self.food + 1
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
local Cat = Animal:inherit()
 | 
			
		||||
 | 
			
		||||
-- Instead, we override initialize() in Cat.
 | 
			
		||||
function Cat:initialize()
 | 
			
		||||
    self:_initialize()
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function Cat:speak()
 | 
			
		||||
    print("meow")
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Having a class library like this makes things a lot more convenient, as we no longer have to mess with raw metatables!
 | 
			
		||||
All we need to do is call `inherit()` and `new()`, and the magic is done for us.
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local kitty = Cat:new()
 | 
			
		||||
kitty:speak()
 | 
			
		||||
kitty:feed()
 | 
			
		||||
print(kitty.food)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Wrapping up
 | 
			
		||||
 | 
			
		||||
If you followed this tutorial from beginning to end, you now have a simple library for object-oriented programming in Lua, which supports creating classes and inheriting from them.
 | 
			
		||||
 | 
			
		||||
To further your understanding, you may want to think about the following:
 | 
			
		||||
 | 
			
		||||
- How would you call the superclass's implementation of a function overridden by the subclass?
 | 
			
		||||
Can you think of ways to make it convenient and easy to remember?
 | 
			
		||||
 | 
			
		||||
- Our class library implements a Ruby-style `Object:new(args)` function for constructing new instances of our class.
 | 
			
		||||
Python, however, uses the syntax `Object(args)` for constructing instances of objects.
 | 
			
		||||
Can you think of a way to make your class library use the Python-style syntax?
 | 
			
		||||
 | 
			
		||||
- Define a 2D vector class using our class library.
 | 
			
		||||
Can you think of a way to make use of Lua's native `+`, `-`, `*`, `/` math operators, instead of named functions like `:add()`, `:sub()`, `:mul()`, `:div()`?
 | 
			
		||||
 | 
			
		||||
- Try implementing an `object:instanceof(Class)` function, which checks that an object instance inherits from a given class.
 | 
			
		||||
 | 
			
		||||
- Lua is a minimalistic, multi-paradigm language.
 | 
			
		||||
Can you think of the benefits and drawbacks towards doing object-oriented programming in Lua?
 | 
			
		||||
 | 
			
		||||
    - What are some problems for which this style of programming would lend itself as particularly good?
 | 
			
		||||
    - and likewise, what are some areas in which this style might not work so well?
 | 
			
		||||
 | 
			
		||||
## Further reading
 | 
			
		||||
 | 
			
		||||
You may wanna check these links out for additional reference.
 | 
			
		||||
 | 
			
		||||
- [The Lua documentation on metatables](https://www.lua.org/manual/5.4/manual.html#2.4)---there's lots of other operators you can overload!
 | 
			
		||||
 | 
			
		||||
- [rxi's `classic` module](https://github.com/rxi/classic/blob/master/classic.lua)---it's an example of a good, but small class library that has all the features you'd ever need.
 | 
			
		||||
| 
						 | 
				
			
			@ -1103,7 +1103,7 @@ th-literate-program[data-mode="output"] {
 | 
			
		|||
    --syntax-keyword2: #02739d;
 | 
			
		||||
    --syntax-operator: #ac4141;
 | 
			
		||||
    --syntax-function: #9940b9;
 | 
			
		||||
    --syntax-literal: #a84983;
 | 
			
		||||
    --syntax-literal: #4c49a8;
 | 
			
		||||
    --syntax-string: #2c7754;
 | 
			
		||||
    --syntax-punct: #6c657b;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue