@Simeon Continuing on the theme of improving Codea’s API around class
I thought I’d put togther an implementation which extends my thoughts above for using a table with a __call
metamethod to create classes instead of a function, which is closer to Codea’s current definition of class
but still includes some of the previously discussed improvements without changing the class model completely.
Whilst writing the code I also found myself considering how it could also apply to userdata types such as vec2
, vec3
, matrix
as there can be times where it can be necessary or convenient to be able to identify a specific userdata type.
After all, they are essentially just classes except they have a special status because of their dual coexistance in Codea’s backend. However even though they are technically classes, because of their specialness I felt it would still be clearer if working with them was handled separately but implemented so that the public interface is consistent with class’s public interface. Some more in depth thoughts and example code example representing the public interface follows after the class implementation.
Class
Features/Changes
- Change
class
from a function to an equivalent table with __call
metamethod.
- Removed upvalues from class table
__call
metamethod.
- Renamed
._base
to .base
on class tables.
- Renamed
.is_a
to .is
to shorten and avoid typing a special character for on-screen keyboard users (an is_a
alias is retained for backwards compatibility).
- Exposed a
class.is
function to enable easier class instance checking when dealing with multiple types e.g class.is(obj, MyClass)
.
- Use
class
as base
for root class tables so that a class instance can be identified by class.is(obj,class)
but class.is({},class) == false
- Added a
.class
member to class tables to enable instance to instance type comparision e.g instance:is(otherinstance.class)
- Added parameter
assert
validations to give clearer errors when misuse occurs.
class = {}
class.is = function(instance, klass)
if type(instance) ~= "table" then return false end
if klass ~= nil and type(klass) ~= "table" then
assert(false,string.format("bad argument #2 to 'class.is' (class table expected, got %s)",type(klass)))
elseif klass == nil then
klass = class
end
local m = getmetatable(instance)
while m do
if m == klass then return true end
m = m.base
end
return false
end
setmetatable(class,{ __call = function(self,base)
if base ~= nil and type(base) ~= "table" then
--avoid string format unless needed
assert(false,string.format("bad argument #1 to 'class' (class table expected, got %s)",type(base)))
end
local c = setmetatable({}, {
__call = function(klass, ...)
local obj = setmetatable({},klass)
if klass.init then
klass.init(obj,...)
end
return obj
end
})
if base then
for i,v in pairs(base) do
c[i] = v
end
end
c.is_a = class.is -- backwards compatibility
c.is = class.is
c.class = c
c.base = base or class
c.__index = c
return c
end
})
Example usage
function setup()
local instance = TestClass1()
local instance2 = TestClass1()
print(string.format("class.is({},class) = %s", class.is({},class)))
print(string.format("class.is(1) = %s", class.is(1)))
print(string.format("class.is(instance,TestClass1) = %s",class.is(instance,TestClass1)))
print(string.format("class.is(instance,TestClass) = %s",class.is(instance,TestClass)))
print(string.format("class.is(instance,class) = %s",class.is(instance,class)))
print(string.format("class.is(instance) = %s",class.is(instance)))
print(string.format("class.is(instance.class, instance2.class) = %s",
class.is(instance, instance2.class)))
end
--[[
class.is({},class) = false
class.is(1) = false
class.is(instance,TestClass1) = true
class.is(instance,TestClass) = true
class.is(instance,class) = true
class.is(instance) = true
class.is(instance.class, instance2.class) = true
]]--
Userdata
Although I realise that identifying userdata types has been discussed quite a bit and there are a number of solutions available. It seems logical that Codea’s API should provide some help with this because of the obscurity of the current solution for identifying them directly which usually works something like this.
- obtain instances of userdata types which need to be identified later.
- call
getmetatable
on the instances to obtain there metatable
- create some form of lookup with the metatables that is keyed with a string name or the function used to create instances.
- expose a function which does something like
getmetatable(instance) == lookup[key]
which is equivalent of class.is_a(SomeType)
or overide type
for example type(vec3) == "userdata", "vec3"
.
Despite its slightly hacky feel the getmetatable
solution is actually pretty effective, up to a point. But it all starts to get a bit messy and less containable to a single tab when you start trying to include metatables from types that can only be obtained via functions called by Codea such as touch
or physics.contact
.
Also considering the fact that CodeaCraft is somewhere on the horizion it seems reasonable to assume that its release will probably increase the number of userdata types that are going to be available. So logically it could potentially also increase the likelihood of users programming themselves into situations where identifying the specific userdata type that they are working with would help them meet their requirments or just genrally be convenient and/or shorter to write.
A simple example is easy to consider. Imagine a function which works with several different userdata types but only userdata types exclusively. Also assume that it performs slighlty different operations depending on the type.
You can validate that a userdata type was passed easily, assert(type(obj)=="userdata","message")
. But to validate which type you have several options:
- use a function with named parameters
- wrapping the values in classes so
is_a
becomes possible
- resort to comparing metatables
All of which obviously just generally increases the overhead (a little) and the amount of code for a user to write or tap out if they don’t use AirCode or a bluetooth keyboard. With the comparing metatables solution probably being the longest and most complex.
Features
- Exposes a
userdata.is
global function which behaves like class.is
- Exposes a
userdata.type
global function which behaves like type
for userdata types only
- Parameter
assert
validations to give clear errors when misuse occurs.
Note: Implementation only supports vec2
,vec3
, vec4
.
userdata = {}
do
-- Ideally userdata should, if possible, use tables with __call metatables to return instances so that
-- userdata.is could effectivly be getmetatable(vec3(0,1,0)) == vec3
-- another alternative could be to use luaL_newmetatable for userdata metatables in Codea's backend
-- it appears that in lua 5.3 it adds a __name key which can be used to lookup the name of the userdata type.
-- e.g getmetatable(vec2(0,0,0)).__name would be "vec2".
-- http://stackoverflow.com/questions/38932374/lua-querying-the-name-of-the-metatable-of-a-userdata-object
-- https://github.com/keplerproject/lua-compat-5.3/issues/13
-- https://www.lua.org/source/5.3/lauxlib.c.html#luaL_newmetatable
local userdataInfo = {
{name="vec2", func = vec2},
{name="vec3", func = vec3},
{name="vec4", func = vec4},
}
for i,item in ipairs(userdataInfo) do
local metatable = item.metatable
if metatable == nil then
metatable = getmetatable(item.func())
end
metatable.__descriptor = {
name = item.name,
func = item.func
}
end
end
userdata.is = function(instance, func)
if type(instance) ~= "userdata" then return false end
if func ~= nil and func ~= userdata and type(func) ~= "function" then
--avoid string format unless needed
assert(false,
string.format("bad argument #2 to 'userdata.is' (userdata function expected, got %s)",type(func)))
elseif func == nil or func == userdata then
return true
end
local descriptor = getmetatable(instance).__descriptor
return descriptor and descriptor.func == func
end
userdata.type = function(instance)
local instanceType = type(instance)
--avoid string format unless needed
if instanceType ~= "userdata" then
assert(false,string.format("bad argument #1 to 'userdata.type' (userdata expected, got %s)",instanceType))
end
local descriptor = getmetatable(instance).__descriptor
return descriptor and descriptor.name or "userdata"
end
Usage Example
function setup()
local vec = vec3(0,1,0)
print(string.format("userdata.is({},vec2) = %s",userdata.is({},vec2)))
print(string.format("userdata.is(1) = %s",userdata.is(1)))
print(string.format("userdata.is(vec, vec2) = %s",userdata.is(vec, vec2)))
print(string.format("userdata.is(vec, vec3) = %s",userdata.is(vec, vec3)))
print(string.format("userdata.is(vec, userdata) = %s",userdata.is(vec, userdata)))
print(string.format("userdata.is(vec) = %s",userdata.is(vec)))
print(string.format("userdata.type(vec) = %s",userdata.type(vec)))
end
--[[
userdata.is({},vec2) = false
userdata.is(1) = false
userdata.is(vec, vec2) = false
userdata.is(vec, vec3) = true
userdata.is(vec, userdata) = true
userdata.is(vec) = true
userdata.type(vec) = vec3
]]--