|
The __mt patch for Lua
1. Understanding the standard metamethods
Metatables are explained in detail in section 2.8 of the
Lua manual, but I found out that a certain abuse of language —
writing A.__mt for the metatable of A — simplifies a lot
the description of what happens in the cases where the metatables are
used.
Let's use the squiggly arrow, "⇝" (or "-~-> " in ascii),
to indicate rewritings. The three rewritings below are
standard:
. T.__foo -~-> T["foo"]
foo:bar(...) -~-> foo.bar(foo, ...)
function foo (...) ... end -~-> foo = function (...) return ... end
|
They are described here:
If A and B are tables having the same non-nil metatable,
then, modulo details (error conditions, etc), we have:
A + B -~-> A.__mt.__add(A, B)
A - B -~-> A.__mt.__sub(A, B)
A * B -~-> A.__mt.__mul(A, B)
A / B -~-> A.__mt.__div(A, B)
A % B -~-> A.__mt.__mod(A, B)
A ^ B -~-> A.__mt.__pow(A, B)
- A -~-> A.__mt.__unm(A, A)
A .. B -~-> A.__mt.__concat(A, B)
#A -~-> A.__mt.__len(A, A)
A == B -~-> A.__mt.__eq(A, B)
A ~= B -~-> not (A == B)
A < B -~-> A.__mt.__lt(A, B)
A <= B -~-> A.__mt.__le(A, B)
(or not (B < A) in the absence of __le)
A[k] -~-> A.__mt.__index(A, k)
A[k] = v -~-> A.__mt.__newindex(A, k, v)
A(...) -~-> A.__mt.__call(A, ...)
tostring(A) -~-> A.__mt.__tostring(A)
Note: the listing above follows the same order as the manual.
See: (find-lua51manual "#2.8" "op1 + op2")
(find-lua51manual "#pdf-tostring")
|
2. The __mt patch: idea
So here's the idea. It shouldn't be too hard to create a patch for
lparser.c (or maybe to use MetaLua or the token
filter thing) that implements the following rewrites,
A.__mt -~-> getmetatable(A) (when A.__mt is an Rvalue)
A.__mt = B -~-> setmetatable(A, B) (when A.__mt is an Lvalue)
|
yet does not change the meaning of arbitrary accesses to the "__mt" field of a table, e.g., in A["__mt"] — so, __mt should only becomes special when it plays the role of "Name" in
the this rule in Lua's grammar:
var ::= Name | prefixexp `[' exp `]' | prefixexp `.' Name
|
That patch would not affect at all the performance of
(byte-)compiled Lua programs, and it would let use notations like
A + B -~-> A.__mt.__add(A, B)
|
as more than pseudo-code...
3. The __mt patch: an implementation
In 2010dec27 I discussed this idea with Marc Simpson,
and he quickly realized the main uses for it would be in teaching and
in doing experiments with metatables — for example, when we are
examining or devising an OO scheme —, and so it makes more
sense to implement it in the simplest possible way, even if the
patched Lua interpreter would run more slowly than a standard one...
and he wrote a working patch, which uses a strcmp to intercept all
accesses to an "__mt" field in tables. His patch is here and here.
4. eoo.lua
Using the __mt notation and Gavin Wraith's "\ " and "=> " shorthands, my favorite OO system, eoo.lua,
becomes easy to explain. Its code is just:
-- (find-blogme4 "eoo.lua")
Class = {
type = "Class",
__call = \ (class, o) => o.__mt = class end,
}
Class.__mt = Class
otype = function (o)
local mt = o.__mt
return mt and mt.type or type(o)
end
|
and here is a test for it:
* (eepitch-lua51)
* (eepitch-kill)
* (eepitch-lua51)
-- (find-angg "LUA/lua50init.lua" "ee_dofile")
-- (find-blogme4 "eoo.lua")
ee_dofile "~/blogme4/eoo.lua"
-- (find-angg ".emacs.templates" "class")
Vector = Class {
type = "Vector",
__add = function (V, W) return Vector { V[1]+W[1], V[2]+W[2] } end,
__tostring = function (V) return "("..V[1]..","..V[2]..")" end,
__index = {
norm = function (V) return math.sqrt(V[1]^2 + V[2]^2) end,
},
}
v = Vector {3, 4}
w = Vector {20, 30}
print(v) --> (3,4)
print(v + w) --> (23,34)
print(v:norm()) --> 5
print( type(v)) --> table
print(otype(v)) --> Vector
print( type("")) --> string
print(otype("")) --> string
|
Here is how it works. We have:
Class = {
type = "Class",
__call = \(class, o) => o.__mt = class end,
__mt = Class
}
Vector = {
type = "Vector",
__add = \(V, W) => {V[1]+W[1], V[2]+W[2]} end,
__tostring = \(V) => "("..V[1]..","..V[2]..")" end,
__index = { norm = \(V) => math.sqrt(V[1]^2+V[2]^2) end },
__mt = Class
}
|
and so:
v = Vector {3, 4}
--~-> Vector({3, 4})
--~-> Vector.__mt.__call(Vector, {3, 4})
--~-> Class.__call(Vector, {3, 4})
--~-> (\(class, o) => o.__mt = class end)(Vector, {3, 4})
--~-> (\() => {3, 4}.__mt = Vector end)()
--~-> {3, 4, __mt = Vector}
|
and then:
v:norm()
--~-> {3, 4, __mt=Vector}:norm()
--~-> {3, 4, __mt=Vector}.norm({3, 4, __mt=Vector})
--~-> {3, 4, __mt=Vector}.__mt.__index.norm({3, 4, __mt=Vector})
--~-> Vector.__index.norm({3, 4, __mt=Vector})
--~-> (\(V) => math.sqrt(V[1]^2+V[2]^2) end)({3, 4, __mt=Vector})
--~-> (\() => math.sqrt( 3^2+ 4^2) end)()
--~-> (\() => 5 end)()
--~-> 5
|
This idea of using reductions is useful in other cases too, that do
not involve metatables. Here is an e-mail that I
sent to Marc in 2010dec29, about a notation for tracking upvalues.
Some of the ideas in it were inspired by Paul Blain Levy's
book.
|