1
Fork 0

add Lua classes tutorial

This commit is contained in:
リキ萌 2025-02-08 22:31:18 +01:00
parent 05e60920a7
commit 7cf5fbf843
3 changed files with 517 additions and 2 deletions
content
programming/lua
treehouse
static/syntax

View file

@ -0,0 +1,495 @@
%% title = "implementing classes in Lua"
% id = "01JKKQHG5DHSAN8FNC27WM0RE5"
- 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()
```
% id = "01JKKQHG5DDM42QXJ6RQJ6MYGG"
- this is the way people generally do object-oriented programming in the language.
% id = "01JKKQHG5DAM1JSKSHDPPT5DZ3"
- for someone coming from a language like Java, where classes are a syntactic construct---`class Cat extends Animal`---it can feel weird to see them declared this way---as local variables, using regular functions to implement inheritance.
% id = "01JKKQHG5DJEJ6EM5NM24EWZW0"
- but worry not!
this tutorial will hopefully clear up any confusion you might have, using beginner-friendly language, and simple examples.
% id = "01JKKQHG5D3M7RY6D9EXMXEB3P"
- ### metatables
% id = "01JKKQHG5DX4PEJ1GW27S257T3"
- before we start, we need to talk about *metatables*.
these are Lua's way of allowing users to _overload operators_.
% id = "01JKKQHG5DBRZA5EAF7D6C11KA"
- operators include arithmetic: `+`, `-`, `*`, `/`, but also things like indexing tables `a[b]`, creating new indices in tables `a[b] = c`, or function calls `a(b, c, d)`.
% id = "01JKKQHG5DWNC80HZAXXQ9JR7Z"
- we call it operator _overloading_, because we _overload_ the default meaning of the operator with our own, custom definition.
% id = "01JKKQHG5D62EMCSEPTQAN82XH"
- we can set the metatable of a table using [`setmetatable(t, metatable)`](https://www.lua.org/manual/5.4/manual.html#pdf-setmetatable).
% id = "01JKKQHG5D86T78ESRTQBEJ5T4"
- the `metatable` is another table, that contains fields for overriding these operators.
% id = "01JKKQHG5DMCH6P2X8C23EHKW0"
- the most important field of metatables we'll be focusing on today is `__index`, which defines a _fallback_ for the `a[b]` operator---and by extension, also `a.b`, which is syntactic sugar for `a["b"]`.
% id = "01JKKQHG5DCNFHQ72NGAE4T12D"
- #### `__index`
% id = "01JKKQHG5DXTZDGY9CHH456388"
- the `__index` field is used when an otherwise `nil` field is accessed in a table.
consider this:
```lua
local t = { a = 1 }
print(t.b) --> nil
```
% id = "01JKKQHG5DR51DKCN70ZJAGBM1"
- in this case, `t` does not have a metatable with `__index`, so `nil` is returned.
to change this behaviour, we override `__index` by telling Lua a function to run whenever the key doesn't exist.
```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
```
% id = "01JKKQHG5DG2EGZTBEZ38YK8KW"
- however, there is a more compact and faster way of doing this.
`__index` is special, because in addition to being able to set it to a function, we can also set it to a table:
```lua
setmetatable(t, {
__index = fallback,
})
print(t.b) --> 2
```
this avoids the need to allocate a local function, which can be costly if you run it many times in a game loop!
% id = "01JKKQHG5DTZG3T5NQYWSQYGQF"
- ### method call syntax
% id = "01JKKQHG5DYJBYRMSK7MJWZT5F"
- there's 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:
```lua
a.method(a, b)
```
% id = "01JKKQHG5D35F2P45MA33QNAER"
- basically, the thing before `:` is passed as the first argument to the thing before `:`'s `method` function.
% id = "01JKKQHG5DC2MDYZ9M07ZZ8J9D"
- Lua also has a syntax sugar for declaring functions on tables:
```lua
local t = {}
function t.do_stuff()
print("hi")
end
```
% id = "01JKKQHG5DTF67PWWTWPFMYT6Q"
- to complement the `:` call syntax, there's also the `:` function declaration syntax.
```lua
function t:do_thing()
self.aaa = 1
end
-- desugars to
function t.do_thing(self)
self.aaa = 1
end
```
as this example shows, this syntax simply inserts a parameter named `self` before all other parameters.
% id = "01JKKQHG5D2QEHBJ32NF0VCFEX"
- the call and declaration syntaxes are not tied together in any way, so the dot and colon syntax could be mixed however one wants, but it's probably better not to.
% id = "01JKKQHG5DNY2EPFN195KYJVEW"
- bear in mind that your function declarations also serve the purpose of documentation, and using the `:` syntax in declarations makes it clearer you're supposed to call the functions with the `:` syntax.
% id = "01JKKQHG5DY4JZDZTB5B7W9D8N"
- with that knowledge, we can move on to creating classes.
% id = "01JKKQHG5DCTRRFQQ7NNNBM8XS"
- ### classes
% id = "01JKKQHG5D4M3K6D4ECF2M2QFD"
- we can use `__index` fallback tables to model classes quite easily.
% id = "01JKKQHG5D8QVHGTDGEP9RYBJG"
- let's create a class `Cat` with two methods `meow` and `feed`:
```lua
local Cat = {}
function Cat:meow()
print("meow")
end
function Cat:feed()
self.food = self.food + 1
end
```
% id = "01JKKQHG5DWP99Q74Q62RVS176"
- we also need a method for creating cats, which I'll call `new`:
```lua
function Cat:new()
local cat = {}
cat.food = 10
return cat
end
```
% id = "01JKKQHG5DDVYTWXZYS711DVWT"
- we can now use the API like this:
```lua
local kitty = Cat:new()
Cat.meow(kitty)
Cat.feed(kitty)
print(kitty.food) --> 11
```
but, note how we have to namespace the `Cat` functions specifically, and we cannot use the `:` method call operator yet.
the table returned by `Cat:new()` does not have the methods `meow` and `feed` for that to work.
% id = "01JKKQHG5D0KT6YAR9MWAC00N0"
- so to provide it with these methods, 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
```
% id = "01JKKQHG5DQZRNTCWK2JACFJWT"
- _now_ we'll be able to create cats that can meow on their own:
```lua
kitty = Cat:new()
kitty:meow()
kitty:feed()
print(kitty.food) --> 11
```
% id = "01JKKQHG5DK3S08JBG24970YV6"
- 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
```
% id = "01JKKQHG5DYG4C3Y5H4WEYQ025"
- but note how we've declared `Cat:new` with the special method syntax.
we call the method like `Cat:new()`, which desugars to `Cat.new(Cat)`, which means that the implicit `self` parameter _is_ already the `Cat` table!
thus, we can simplify the call to `setmetatable`, to remove the redundant reference to `Cat`:
```lua
return setmetatable(cat, self)
```
% id = "01JKKQHG5D1ZTAT0JPNPWDE2WC"
- 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
```
% id = "01JKKQHG5DHXF00NJPPVXBZ98K"
- ### inheritance
% id = "01JKKQHG5DB3JWK6YCG1N47JZ1"
- given this fairly simple way of creating classes, we can now expand this idea to inheritance.
% id = "01JKKQHG5D7N0A33S109YF2NV6"
- conceptually, inheriting from a class is pretty simple: 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.
% id = "01JKKQHG5DSB0DQPPWQZNPWY1M"
- let's rewrite our example with the kitty to generalise animals under a single class:
```
Animal
- food: integer
: speak()
: feed()
Cat : Animal
: speak()
```
% id = "01JKKQHG5D4PZMTFRFV4T6HSA1"
- so, 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.
function Animal:speak() error("not implemented") end
function Animal:feed()
self.food = self.food + 1
end
```
% id = "01JKKQHG5D0ENDQ5QQTBAA79GQ"
- we can define a `Cat` class as a subclass of `Animal`:
```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.
return setmetatable({
food = 1,
}, self)
end
-- Don't forget to override speak(), otherwise calling it will error out!
function Cat:speak()
print("meow")
end
```
% id = "01JKKQHG5DCHD37G3WKEBJA5Z6"
- note now that declaring `speak` _does not modify `Animal`_.
for that, we would need to set the _`__newindex`_ metatable field on the `Animal`, not just `__index`.
% id = "01JKKQHG5D08QQ8XSXBRMCDV26"
- now we can create instances of the `Cat`, and it will inherit the `feed` method from `Animal`:
```lua
local kitty = Cat:new()
kitty:speak()
kitty:feed()
print(kitty.food) --> 2
```
% id = "01JKKQHG5DYZW7W7Q9H75HN8BM"
- ### generalising
% id = "01JKKQHG5DTATR750GQ2S7EAPF"
- with all this, we are now ready to pack this subclassing functionality into a nicer package.
speaking of packages, let's create a module `class.lua`:
```lua
local Class = {}
Class.__index = Class
return Class
```
% id = "01JKKQHG5DFTHS8P07T3KET5F8"
- now, let's create a method 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 method like `SomeClass:inherit()`.
setmetatable(Subclass, self)
return Subclass
end
```
% id = "01JKKQHG5DTAK0EA4PR6JX2GV8"
- this is going to let us cleanly inherit from classes, without needing to copy and paste all the `__index` and `setmetatable` boilerplate:
```lua
local Class = require "class"
local Sub = Class:inherit()
```
% id = "01JKKQHG5DKY7ZFWA42D9NJ49C"
- the other boilerplaty 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 method 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
```
% id = "01JKKQHG5D8VD9E0SXCY337D3W"
- 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
function Animal:speak()
error("unimplemented")
end
function Animal:feed()
self.food = self.food + 1
end
---
local Cat = Animal:inherit()
-- Don't forget that our initialize() method errors by default, so it has to be overridden.
function Cat:initialize()
self:_initialize()
end
function Cat:speak()
print("meow")
end
```
% id = "01JKKQHG5DFS98T9X8A8SPYXCT"
- having a nice class library like this makes things a lot more convenient.
no longer do we have to mess with raw metatables!
all we need to do is call `inherit()` or `new()`, and the magic is done for us.
```lua
local kitty = Cat:new()
kitty:speak()
kitty:feed()
print(kitty.food)
```
% id = "01JKKQHG5DSR2Z0XGW6W2H97NH"
- ### wrapping up
% id = "01JKKQHG5DANR0QF460EHKAWMP"
- 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.
% id = "01JKKQHG5DMCXV1VT37Z8M7P5G"
- to further your understanding, you may want to think about the following:
% id = "01JKKQHG5DRF6RVFG4P9XYDXZZ"
- how would you call the superclass's implementation of a method?
can you think of ways to make it convenient and easy to remember?
% id = "01JKKQHG5DQW7G1HDWYGX4WEYC"
- 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 our class library use the Python-style syntax?
% id = "01JKKQHG5D7QN029A3F1B6SYC2"
- 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 methods `:add()`, `:sub()`, `:mul()`, `:div()`?
% id = "01JKKQHG5DJP0C1V8K2BQBD6G2"
- try implementing an `object:instanceof(Class)` function, which checks that an object instance inherits from a given class.
% id = "01JKKQHG5DN41D9BME6JXJ3R86"
- Lua is a minimalistic, multi-paradigm language.
can you think of any benefits and drawbacks towards doing object-oriented programming in Lua?
% id = "01JKKQHG5DNRKVQJAVN4KP608R"
- what are some problems for which this style of programming would lend itself as particularly good?
% id = "01JKKQHG5D6TY9QSCQGEQWMZGM"
- and similarly, what are some areas in which this style might not work so well?
% id = "01JKKQHG5DFNRGD6D17RHB3S22"
- ### further reading
% id = "01JKKQHG5DDB411SKNFA8M7K2B"
- you may wanna check these out for additional reference.
% id = "01JKKQHG5DRWGGE7GE3GMCB7E8"
- [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!
% id = "01JKKQHG5D3PQZY439H61FN27V"
- [rxi's `classic`](https://github.com/rxi/classic/blob/master/classic.lua) module---it's an example of a good, but small class library that has all the features you'd ever need.

View file

@ -26,7 +26,26 @@ if you've been wondering what I've been up to, you've come to the right place.
if you want to read any of the posts, follow the links. if you want to read any of the posts, follow the links.
it's like that by design. it's like that by design.
% tags = ["programming", "lua"]
id = "01JKKQZRSG5ZRNH530D75E2660"
- ### [implementing classes in Lua][page:programming/lua/classes]
% id = "01JKKQZRSGXT99FJ7V3BGKE2D5"
- one of my absolutely favourite parts of Lua is how tiny, but capable it is.
did you know you could implement object-oriented programming without needing any additional syntactic support?
% id = "01JKKQZRSGMN81PXJWGP5Y17WY"
- this is a remaster of an [old tutorial I published as a Gist](https://gist.github.com/liquidev/3f37f94efdacd14a654a4bdc37c8008f) to explain how object-oriented programming works in Lua to someone on the [LÖVE](https://love2d.org/) Discord server.
% id = "01JKKQZRSGMW486TGSXQVM8FV3"
- thus there's a high likelihood you've never read it.
however, I think it's a pretty nice tutorial, so I'm republishing it here outside the shackles of GitHub.
% id = "01JKKQZRSGNA5NRXR5H6WPZKWA"
- and the programming trickery in it might just open your third eye a bit, so you should read it even if you're not into Lua!
% id = "01JK5SN2ZBDZTFZ27J3KNT4SQV" % id = "01JK5SN2ZBDZTFZ27J3KNT4SQV"
tags = ["music"]
- ### [Floating Points - Tilt Shift / Ablaze][page:music/tilt-shift-ablaze] - ### [Floating Points - Tilt Shift / Ablaze][page:music/tilt-shift-ablaze]
% id = "01JK5SNYKRK08F5DJM4JGBK4C4" % id = "01JK5SNYKRK08F5DJM4JGBK4C4"
@ -38,6 +57,7 @@ if you've been wondering what I've been up to, you've come to the right place.
so that I can remember, and so the world can see, too... so that I can remember, and so the world can see, too...
% id = "01JHXVRT2HR6TXC2V9JG2XTZVB" % id = "01JHXVRT2HR6TXC2V9JG2XTZVB"
tags = ["music"]
- ### [The Flashbulb - Flacks / aBliss][page:music/flacks] - ### [The Flashbulb - Flacks / aBliss][page:music/flacks]
% id = "01JHXVRT2H2CTGBEDYWCMDTTS3" % id = "01JHXVRT2H2CTGBEDYWCMDTTS3"

View file

@ -19,7 +19,7 @@
}, },
{ "regex": "0[xX]\\.[0-9a-fA-F]+([pP][-+]?[0-9]+)?", "is": "literal" }, { "regex": "0[xX]\\.[0-9a-fA-F]+([pP][-+]?[0-9]+)?", "is": "literal" },
{ {
"regex": "[0-9][0-9_]+(\\.[0-9_]*([eE][-+]?[0-9_]+)?)?", "regex": "[0-9]+(\\.[0-9]*([eE][-+]?[0-9]+)?)?",
"is": "literal" "is": "literal"
}, },
{ {
@ -30,7 +30,7 @@
} }
}, },
{ "regex": "\\.\\.\\.", "is": "punct" }, { "regex": "\\.\\.\\.", "is": "punct" },
{ "regex": "[+=/*^%#<>~.-]+", "is": "operator" }, { "regex": "[+=/*^%#<>~.:-]+", "is": "operator" },
{ {
"regex": "([a-zA-Z_][a-zA-Z0-9_]*)\\(", "regex": "([a-zA-Z_][a-zA-Z0-9_]*)\\(",
"is": { "is": {