Better documentation of the object-oriented-lua patch, and removal of an unused patch from lua
This commit is contained in:
336
Docs/Object-Oriented-Lua.md
Normal file
336
Docs/Object-Oriented-Lua.md
Normal file
@@ -0,0 +1,336 @@
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user