337 lines
9.7 KiB
Markdown
337 lines
9.7 KiB
Markdown
|
|
|
||
|
|
## How to do Methods and Inheritance in Standard Lua
|
||
|
|
|
||
|
|
Standard Lua has the ability to do classes and
|
||
|
|
inheritance. Let me explain how.
|
||
|
|
|
||
|
|
Lua has a colon operator, for method lookup. It looks
|
||
|
|
like this:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
local result = v1:dotproduct(v2)
|
||
|
|
```
|
||
|
|
|
||
|
|
By default, this looks for the method "dotproduct" inside
|
||
|
|
the table v1. But you don't really want to put a whole
|
||
|
|
bunch of function objects (like dotproduct) into v1, and
|
||
|
|
into every other vector. Instead, you want to put those
|
||
|
|
function objects in a separate table, the class table:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
vector = {}
|
||
|
|
function vector.dotproduct(v1, v2) ... end
|
||
|
|
function vector.crossproduct(v1, v2) ... end
|
||
|
|
```
|
||
|
|
|
||
|
|
Fortunately, Lua can do that. Lua has a metamethod, INDEX,
|
||
|
|
that basically says: "if you don't find what you're looking
|
||
|
|
for in this table, look in that table instead." We can use
|
||
|
|
the INDEX metamethod to tell lua: if you're looking a method
|
||
|
|
"dotproduct" in v1, and it's not there, look in class vector
|
||
|
|
instead.
|
||
|
|
|
||
|
|
So to do that, you only need two steps. First, we're going
|
||
|
|
to decide that class vector isn't just hold the methods
|
||
|
|
for vectors. We're also going to use it as the metatable
|
||
|
|
for all vectors:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
v1 = {x=100, y=100, z=100}
|
||
|
|
v2 = {x=200, y=200, z=200}
|
||
|
|
setmetatable(v1, vector)
|
||
|
|
setmetatable(v2, vector)
|
||
|
|
```
|
||
|
|
|
||
|
|
Now that vector is the metatable for all vectors, you can
|
||
|
|
put metamethods into class vector and they will affect all
|
||
|
|
vectors. We're going to put an __INDEX metamethod in there:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
vector = {}
|
||
|
|
vector.__INDEX = vector
|
||
|
|
function vector.dotproduct(v1, v2) ... end
|
||
|
|
function vector.crossproduct(v1, v2) ... end
|
||
|
|
```
|
||
|
|
|
||
|
|
That says: if you're doing a lookup on a vector,
|
||
|
|
and the lookup fails, look in class vector instead.
|
||
|
|
|
||
|
|
OK, in case you didn't quite follow, let me walk you through
|
||
|
|
step by step what happens when you evaluate the expression
|
||
|
|
v1:dotproduct(v2).
|
||
|
|
|
||
|
|
First, the colon operator looks for "dotproduct" in v1.
|
||
|
|
It's not going to find it: we're not going to put all
|
||
|
|
those function objects into every single vector.
|
||
|
|
|
||
|
|
When the table lookup code fails to find "dotproduct" in v1,
|
||
|
|
it goes to a fallback codepath: it checks whether v1 has a
|
||
|
|
metatable. It does: vector is the metatable for all vectors.
|
||
|
|
So v1 does indeed have a metatable. The table lookup
|
||
|
|
code then checks whether the metatable contains INDEX:
|
||
|
|
again, it does, class vector contains INDEX. Finally,
|
||
|
|
the table lookup checks what INDEX is pointing to:
|
||
|
|
class vector again. So the table lookup code, having
|
||
|
|
failed to find "dotproduct" in v1, looks for it in
|
||
|
|
class vector. The method lookup succeeds.
|
||
|
|
|
||
|
|
The little trick "vector.__INDEX = vector" certainly feels
|
||
|
|
arcane, but it works. I like to hide it with a little
|
||
|
|
syntactic sugar:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
def makeclass(name)
|
||
|
|
_G[name] = {}
|
||
|
|
_G[name].__INDEX = _G[name]
|
||
|
|
end
|
||
|
|
```
|
||
|
|
|
||
|
|
Now I can say this, and it hides all the metatable magic:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
makeclass("vector")
|
||
|
|
function vector.dotproduct(v1, v2) ... end
|
||
|
|
function vector.crossproduct(v1, v2) ... end
|
||
|
|
```
|
||
|
|
|
||
|
|
A useful property of this design is that vector is *both*
|
||
|
|
the metatable for all vectors, and *also* the class table
|
||
|
|
for all vectors. One consequence of that is that I can
|
||
|
|
put both regular methods and metamethods in there:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
makeclass("vector")
|
||
|
|
function vector.__eq(v1, v2) ... end
|
||
|
|
function vector.dotproduct(v1, v2) ... end
|
||
|
|
function vector.crossproduct(v1, v2) ... end
|
||
|
|
```
|
||
|
|
|
||
|
|
Let's say you also want inheritance: you want vector to
|
||
|
|
inherit from vectorbase. That's another thing you can
|
||
|
|
accomplish using INDEX again: "if you can't find what you're
|
||
|
|
looking for in vector, look in vectorbase." Here's the code
|
||
|
|
for that:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
makeclass("vectorbase")
|
||
|
|
function vectorbase.whatever(...) ... end
|
||
|
|
|
||
|
|
makeclass("vector")
|
||
|
|
function vector.dotproduct(v1, v2) ... end
|
||
|
|
function vector.crossproduct(v1, v2) ... end
|
||
|
|
|
||
|
|
setmetatable(vector, vectorbase)
|
||
|
|
```
|
||
|
|
|
||
|
|
That 'setmetatable' directs all failed lookups from
|
||
|
|
vector into vectorbase. I've set up a search path.
|
||
|
|
|
||
|
|
That's really all there is to it. Honestly, it's
|
||
|
|
reasonably straightforward.
|
||
|
|
|
||
|
|
## Why I Don't Love It: High Bug Potential
|
||
|
|
|
||
|
|
Let's say you want to create a class, "json". This is
|
||
|
|
for receiving and transmitting json over HTTP:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
json={}
|
||
|
|
function json.validate() ... end
|
||
|
|
function json.serialize(output) ... end
|
||
|
|
```
|
||
|
|
|
||
|
|
Now, suppose you receive this json from an HTTP client:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"jsonrpc" : "2.0",
|
||
|
|
"validate" : true
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
You convert the json into a lua table *request*, call
|
||
|
|
setclass(request, json), and then you try to validate
|
||
|
|
the request:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
bool legal = request:validate()
|
||
|
|
```
|
||
|
|
|
||
|
|
This produces an error: "true" is not a callable function.
|
||
|
|
That's because when it looks for the "validate" method,
|
||
|
|
it instead finds the "validate" data that is in the request.
|
||
|
|
|
||
|
|
The underlying problem is this: method lookups should
|
||
|
|
*only* look in the class table, not in the request.
|
||
|
|
Meanwhile, data lookup should be *only* in the request,
|
||
|
|
not in the class table. But the INDEX metamethod
|
||
|
|
doesn't differentiate between method lookups and data
|
||
|
|
lookups. All lookups are treated the same. So all
|
||
|
|
accesses go to *both* tables.
|
||
|
|
|
||
|
|
Using this design for object-orientation means you're always
|
||
|
|
at risk of data hiding your methods, at unexpected times.
|
||
|
|
It also means you can get false readings on instance
|
||
|
|
variables if the actual instance variable is nil.
|
||
|
|
|
||
|
|
## Why I Don't Love It: Speed
|
||
|
|
|
||
|
|
How much does this cost:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
local result = v1:dotproduct(v2)
|
||
|
|
```
|
||
|
|
|
||
|
|
The answer is: it has to do three table lookups for just
|
||
|
|
the method lookup:
|
||
|
|
|
||
|
|
- v1["dotproduct"]
|
||
|
|
- metatable["__INDEX"]
|
||
|
|
- vector["dotproduct"]
|
||
|
|
|
||
|
|
That also applies to data lookups. Suppose I do this:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
local param = request.param
|
||
|
|
```
|
||
|
|
|
||
|
|
That's a simple data lookup. But if the request is of class
|
||
|
|
json, and the request doesn't contain "param", then it does
|
||
|
|
three table lookups, trying to find the data "param":
|
||
|
|
|
||
|
|
- request["param"]
|
||
|
|
- metatable["__INDEX"]
|
||
|
|
- json["param"]
|
||
|
|
|
||
|
|
Looking for param in the class table, aside from being
|
||
|
|
risky, is wasting CPU time. This makes lua, an already slow
|
||
|
|
interpreted language, significantly slower.
|
||
|
|
|
||
|
|
## A Patch
|
||
|
|
|
||
|
|
To solve these problems, we need to establish a rule: method
|
||
|
|
lookups should *only* go to the class table, and data
|
||
|
|
lookups should *only* go to the data table.
|
||
|
|
|
||
|
|
To make that happen, I introduce a new metamethod, CLASS
|
||
|
|
(with leading underscores). This metamethod changes *only*
|
||
|
|
the behavior of the method look up operator (colon):
|
||
|
|
|
||
|
|
```lua
|
||
|
|
v1:dotproduct(v2)
|
||
|
|
```
|
||
|
|
|
||
|
|
The behavior is: if you do a method lookup, then the colon
|
||
|
|
operator looks to see if v1 has a metatable with the CLASS
|
||
|
|
metamethod in it. If so, then it looks for the method in
|
||
|
|
the *metatable* of v1 instead of looking in v1.
|
||
|
|
|
||
|
|
Let me show you how this is intended to be used. We create
|
||
|
|
the class table in pretty much the same way, but instead
|
||
|
|
of putting INDEX in there, we put CLASS:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
vector = {}
|
||
|
|
vector.__CLASS = true
|
||
|
|
function vector.dotproduct(v1, v2) ... end
|
||
|
|
function vector.crossproduct(v1, v2) ... end
|
||
|
|
```
|
||
|
|
|
||
|
|
We again intend to make vector the metatable for all
|
||
|
|
vectors. So again, when you create a vector, you do
|
||
|
|
this:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
v1 = {x=100, y=100, z=100}
|
||
|
|
v2 = {x=200, y=200, z=200}
|
||
|
|
setmetatable(v1, vector)
|
||
|
|
setmetatable(v2, vector)
|
||
|
|
```
|
||
|
|
|
||
|
|
So far, it looks almost identical. But when you do
|
||
|
|
v1:dotproduct(v2), the method lookup checks whether v1 has a
|
||
|
|
metatable (it does), and whether that metatable has CLASS in
|
||
|
|
it (it does). So therefore, it *doesn't* look for dotproduct
|
||
|
|
in v1. It *only* looks for it in the metatable of v1, which
|
||
|
|
is class vector.
|
||
|
|
|
||
|
|
The CLASS metamethod has no effect on data lookups. That
|
||
|
|
incudes the dot operator, and the array lookup operator:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
local param = request.param
|
||
|
|
|
||
|
|
local param = request["param"]
|
||
|
|
```
|
||
|
|
|
||
|
|
Even if request above has the json class as its metatable,
|
||
|
|
these lookups are just plain lua table lookups, and that's
|
||
|
|
all. Class json isn't involved.
|
||
|
|
|
||
|
|
This cleanly separates the two namespaces: data is looked
|
||
|
|
up in the data table, methods are looked up in the methods
|
||
|
|
table.
|
||
|
|
|
||
|
|
So that leaves inheritance. We use a slightly different
|
||
|
|
version of the CLASS metamethod:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
vector = {}
|
||
|
|
vector.__CLASS = vectorbase
|
||
|
|
function vector.dotproduct(v1, v2) ... end
|
||
|
|
function vector.crossproduct(v1, v2) ... end
|
||
|
|
```
|
||
|
|
|
||
|
|
Saying CLASS=true means "this is a class." Saying
|
||
|
|
CLASS=vectorbase means "this is a class, derived from
|
||
|
|
vectorbase". If you do that, then the method look-up
|
||
|
|
operator will look in vector first, then vectorbase, and it
|
||
|
|
will follow the inheritance chain upward.
|
||
|
|
|
||
|
|
How does the CLASS metamethod compare, performance-wise, to
|
||
|
|
using the INDEX metamethod to achieve object-orientation?
|
||
|
|
Let's start with data lookups. Using INDEX, this can take
|
||
|
|
up to three table lookups:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
local param = request.param
|
||
|
|
```
|
||
|
|
|
||
|
|
However, using CLASS instead, this is only ever one table
|
||
|
|
lookup. You might assume that since request has a
|
||
|
|
metatable, that lua would at least have to do a table lookup
|
||
|
|
to *see* if there's an INDEX metamethod, even if there's not
|
||
|
|
one. However, lua has a clever optimization: lua remembers
|
||
|
|
that INDEX is not present in class json, and it doesn't
|
||
|
|
look a second time. It only ever does the INDEX lookup
|
||
|
|
once, for the entire program.
|
||
|
|
|
||
|
|
Method lookups like v1:dotproduct(v2) are also accelerated.
|
||
|
|
Using INDEX, it takes three table lookups to find
|
||
|
|
"dotproduct" in class vector. Using CLASS, however, it
|
||
|
|
usually takes only one table lookup. It skips looking in v1
|
||
|
|
entirely. And, like INDEX, lua remembers that CLASS=true is
|
||
|
|
present in class vector, and it doesn't look a second time.
|
||
|
|
So it goes straight to looking for "dotproduct" in class
|
||
|
|
vector. It only has to lookup CLASS if the method is not
|
||
|
|
found, and we have to walk the inheritance tree.
|
||
|
|
|
||
|
|
Finally, I'd like to say something about readability:
|
||
|
|
|
||
|
|
```lua
|
||
|
|
vector.__CLASS = true
|
||
|
|
```
|
||
|
|
|
||
|
|
## Summary
|
||
|
|
|
||
|
|
Metamethod CLASS achieves object-oriented method lookup,
|
||
|
|
but with two advantages over using INDEX:
|
||
|
|
|
||
|
|
* Safely separates the two namespaces: data, and methods.
|
||
|
|
|
||
|
|
* CLASS is faster than using INDEX.
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|