From c6129298252c9763da13c246f49c14231ac10147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Sun, 17 Aug 2025 22:22:00 +0200 Subject: [PATCH] programming/lua/classes: prose version it's much more readable is it not --- content/programming/lua/classes.dj | 467 +++++++++++++++++++++++++++++ static/css/main.css | 2 +- 2 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 content/programming/lua/classes.dj diff --git a/content/programming/lua/classes.dj b/content/programming/lua/classes.dj new file mode 100644 index 0000000..565470e --- /dev/null +++ b/content/programming/lua/classes.dj @@ -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. diff --git a/static/css/main.css b/static/css/main.css index f703844..3496a30 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -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; }