lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


Sam Roberts <sroberts@bycast.com> wrote:

> Does anybody have any links to such an OO library?
>
> I don't need inheritance, but I am looking for a pure-lua solution
> (i.e., not token filter or compiler hacks).

I'm not sure if this is what you're looking for, but here goes:

-----------------------------------------------------------------------
-----------------------------------------------------------------------
-----------------------------------------------------------------------
-----------------------------------------------------------------------
-----------------------------------------------------------------------

-- Cache standard functions.
local assert = assert;
local ipairs, pairs = ipairs, pairs;
local pcall = pcall;
local setmetatable = setmetatable;
local type = type;

-- Cache extension functions.
local GetFEnv, IsCallable, NewUserData = GetFEnv, IsCallable, NewUserData;

---------------------
-- Class definitions
---------------------
local _Defs = {};

-----------------------
-- Internal lookup key
-----------------------
local _Key = _Defs;

------------------
-- Metamethod set
------------------
local _Metamethods = {};    
for _, name in ipairs{
    "__index", "__newindex",
    "__eq", "__le", "__lt",
    "__add", "__div", "__mul", "__sub",
    "__mod", "__pow", "__unm",
    "__call", "__concat", "__gc", "__len"
} do
    _Metamethods[name] = true;
end

---------------------------------------------------------
-- Stack of instances being made by active new functions
---------------------------------------------------------
local _NewStack = {};

-- Type
-- Gets the instance type
-- I: Instance handle
-- Returns: Type name
--------------------------
local function Type (I)
    return GetFEnv(I)[_Key];
end

--------------------------
-- Stock instance methods
--------------------------
local _Stock = {
    -- Use standard __index/__newindex methods.
    __index = false,
    __newindex = false,

    -- Use the local type method.
    Type = Type,

    -- Invokes a method if it exists
    -- I: Instance handle
    -- method: Method name
    -- ...: Method arguments
    -- Returns: Results of call
    ---------------------------------
    Invoke_ = function(I, method, ...)
        assert(method ~= nil);
        local func = I[method];
        if func then
            assert(IsCallable(func), "Uncallable member");
            return func(I, ...);
        end
    end,

    -- Indicates whether an instance is of a given type
    -- I: Instance handle
    -- what: Type to test
    -- Returns: If true, instance matches type
    ----------------------------------------------------
    IsType = function(I, what)
        assert(what ~= nil);

        -- Begin with the instance type. Progress up the base types until a match or the top.
        local ctype = Type(I);
        while ctype ~= nil and ctype ~= what do
            ctype = _Defs[ctype].base;
        end

        -- Succeed on matches.
        return ctype == what;
    end,

    -- Invokes a superclass constructor
    -- I: Instance handle
    -- base: Base type name
    -- ...: Constructor arguments
    ------------------------------------
    SCons = function(I, base, ...)
        assert(base ~= nil);

        -- Ensure that the instance is not of the base type.
        local ctype = Type(I);
        assert(base ~= ctype, "Instance already of base type");

        -- Ensure that the instance derives from the base type.
        repeat
            ctype = _Defs[ctype].base;
            assert(ctype ~= nil, "Base type not found");
        until base == ctype;

        -- Ensure that the call can be traced to a new function.
        assert(_NewStack[#_NewStack] == I, "Invoked outside of new function");

        -- Invoke the constructor.
        _Defs[base].new(I, ...);
    end
};

-- Index
-- Common __index body
-- I: Instance handle
-- key: Lookup key
--------------------------
local function Index (I, key)
    assert(key ~= _Key);
    local env = GetFEnv(I)
    return env[key] or _Defs[env[_Key]].methods[key];
end

-- IndexComposite
-- Builds a composite __index closure
-- first: Function or object to try first
-- Returns: __index closure
------------------------------------------
local function IndexComposite (first)
    return function(I, key)
        assert(key ~= _Key);

        -- Do a call/index of the supplied function/object.
        local result;
        if IsCallable(first) then
            result = first(I, key);
        else
            result = first[key];
        end

        -- Return the result if available. Otherwise, index the instance.
        return result or Index(I, key);
    end
end

-- NewIndex
-- Common __newindex body
-- I: Instance handle
-- key: Lookup key
-- value: Value to assign
--------------------------
local function NewIndex (I, key, value)
    assert(key ~= _Key);
    GetFEnv(I)[key] = value;
end

-- NewIndexComposite
-- Builds a composite __newindex closure
-- first: Function or table to try first
-- Returns: __newindex closure
-----------------------------------------
local function NewIndexComposite (first)
    return function(I, key, value)
        assert(key ~= _Key);

        -- If a function is supplied, call it. A non-nil result can be used to indicate
        -- or fake non-assignment, in which case normal assignment is done.
        if IsCallable(first) then
            if first(I, key, value) ~= nil then
                NewIndex(I, key, value);
            end

        -- Otherwise, assign to the supplied object.
        else
            first[key] = value;
        end
    end
end

-- Define
-- Defines a new class
-- ctype: Type name
-- methods: Method table
-- new: New function
-- params: Configuration parameters
------------------------------------
local function Define (ctype, methods, new, params)
    assert(ctype ~= nil);
    assert(type(methods) == "table", "Non-table methods");
    assert(IsCallable(new), "Uncallable new");
    assert(not _Defs[ctype], "Class already defined");

    -- Prepare the definition.
    local def = { meta = {}, methods = {}, new = new };

    -- Configure the definition according to the input parameters.
    if params then
        assert(type(params) == "table", "Non-table parameters");

        -- Query the size.
        local size = params.size;

        -- Inherit from base class, if provided.
        if params.base ~= nil then
            local baseinfo = assert(_Defs[params.base], "Base class does not exist");

            -- Inherit base class metamethods.
            for key, method in pairs(baseinfo.meta) do
                def.meta[key] = method;
            end

            -- Inherit base class methods.
            setmetatable(def.methods, { __index = baseinfo.methods });

            -- Inherit the size if unspecified.
            size = size or baseinfo.size;

            -- Store the base class name.
            def.base = params.base;
        end

        -- If the class has a shared table, allocate it.
        if params.bShared then
            def.shared = { [_Key] = ctype };
        end

        -- Assign userdatum size.
        if size then
            assert(type(size) == "number", "Non-numeric size");
            assert(size >= 0, "Negative size");
            def.size = size;
        end
    end

    -- Iterate over stock and user methods, where the latter may overwrite stock methods.
    -- By default, target the methods table.
    for _, set in ipairs{ _Stock, methods } do
        for key, method in pairs(set) do
            local mtable, mtype = def.methods;

            -- If a metamethod is specified, target the metamethod table instead.
            if _Metamethods[key] then
                mtable = def.meta;

                -- If an __index or __newindex metamethod is to be added, validate and bind
                -- it; if instead it is to be removed, restore the default(when loading stock
                -- methods, begin by looking for an inherited metamethod).
                if key == "__index" or key == "__newindex" then
                    local mtype = type(method);
                    assert(method == false or IsCallable(method) or mtype == "table" or mtype == "userdata", "Invalid " .. key .. " method");
                    if key == "__index" then
                        method = method and IndexComposite(method) or (set == _Stock and def.meta[key] or Index);
                    else
                        method = method and NewIndexComposite(method) or (set == _Stock and def.meta[key] or NewIndex);
                    end
                end
            end

            -- Install/remove the method into/from its table.
            assert(method == false or IsCallable(method));
            mtable[key] = method or nil;
        end
    end

    -- Register the class.
    _Defs[ctype] = def;
end

-- Indicates whether an item is a class instance
-- I: Item
-- Returns: If true, item is an instance
-------------------------------------------------
local function IsInstance (I)
    if type(I) == "userdata" then
        local env = GetFEnv(I);
        return (env and env[_Key]) ~= nil;
    end
    return false;
end

-- Instantiates a class
-- ctype: Type name
-- ...: Constructor arguments
-- Returns: Instance handle
------------------------------
local function New (ctype, ...)
    assert(ctype ~= nil);

    -- Allocate a userdatum of the type's size. Bind an environment; if available, use the
    -- shared environment. Supply metamethods and methods.
    local typeinfo = assert(_Defs[ctype], "Type not found");
    local I, env = NewUserData(typeinfo.size or 0, typeinfo.shared or { [_Key] = ctype }, typeinfo.meta);

    -- Allow base constructor invocations.
    _NewStack[#_NewStack + 1] = I;

    -- Invoke the new function.
    local bSuccess, message = pcall(typeinfo.new, I, ...);

    -- Disallow base constructor invocations.
    _NewStack[#_NewStack] = nil;

    -- Resolve any errors.
    assert(bSuccess, message);

    -- Return the instance.
    return I;
end

-- Supers
-- Gets the base types of a given type
-- ctype: Type name
-- Returns: Base type names
---------------------------------------
local function Supers (ctype)
    assert(ctype ~= nil);
    return assert(_Defs[ctype]).base;
end

-- Export the class interface.
class = {
    define = Define,
    isinstance = IsInstance,
    new = New,
    supers = Supers
};

-----------------------------------------------------------------------
-----------------------------------------------------------------------
-----------------------------------------------------------------------
-----------------------------------------------------------------------
-----------------------------------------------------------------------

And, on the C-side:

static int GetFEnv (lua_State * L)
{
    lua_getfenv(L, 1);// object, env

    return 1;
}

static int IsCallable (lua_State * L)
{
    lua_pushboolean(lua_isfunction(L, 1) || luaL_getmetafield(L, index, "__call") != 0);// bCallable

    return 1;
}

static int NewUserData (lua_State * L)
{
    lua_newuserdata(L, uI(L, 1));    // size, env, meta, ud
    lua_replace(L, 1);    // ud, env, meta
    lua_setmetatable(L, 1);    // ud, env
    lua_setfenv(L, 1);    // ud

    return 1;
},

which you can lua_register() or whatever.

-----------------------------------------------------------------------

Some things are still a work in progress. I tried to set it up for module/
require, but have yet to make the move. Also, I mean to hide the GetFEnv
and NewUserData, perhaps in some kind of "class.core" DLL as recommended in
a recent thread. And some stuff reflects a forthcoming multiple inheritance
revision.

> Also, if it included examples of how to build behaviourly identical
> "classes" from C based on user-data, particularly ones that are
> "extensible", that would sure be nice...
>
> Sam

Usage:

class.define("MyClass", {
    Method1 = function(MC, x)
        MC.a = x;
    end,
    __len = function(MC)
        return MC.count;
    end
}, function(MC, a)
    MC.a = a;
    MC.count = 4;
end);

class.define("MyClassChild", {
    Method2 = function(MCC)
        return MCC.a * 2;
    end
}, function(MCC, a, b)
    MCC.b = b;
    MCC:SCons("MyClass", a);
end, { base = "MyClass" });

local mcc = class.new("MyClassChild", 2, 4);

print(mcc.a)                --> 2
print(mcc.b)                --> 4
mcc:Method1(3);
print(mcc.a)                --> 3
print(mcc:Method2());            --> 6
print(#mcc);                --> 4
print(mcc:Type());            --> "MyClassChild"
print(mcc:IsType("MyClass"))        --> true
print(mcc:IsType("NotMyClass"))    --> false
print(class.supers("MyClassChild"))    --> "MyClass"
mcc:Invoke_("Method2")            --> returns 6
mcc:Invoke_("Method3")            --> no-op

> "extensible" - I mean that I can add/replace methods of the class at
> run-time, and that I can also add/replace methods on specific instances
> of the class at run-time.

As far as methods on specific instances go, you can just assign them like
regular fields:

mcc.Method2 = function() print("Hi!") end
mcc:Method2();    --> "Hi!"
mcc.Method2 = nil;
mcc:Method2();    --> returns 6

I actually had the add/replace for the class, but merged it back into
class.define since I wasn't using it separately. :P To do that, something
like this should suffice:

local function Change (ctype, methods)
    assert(ctype ~= nil);
    assert(type(methods) == "table", "Non-table methods");    
    local def = assert(_Defs[ctype]);
    for key, method in pairs(methods) do

        <body>

    end
end

and then, for <body>, put everything from the "for key, method in pairs(set) do" block(in
the Define function). Then just add this function to the "class" table.

When you want to remove a method, simply put it in the methods argument, with its name as
the key and false as the value.

The type name can be any non-nil value, not just strings. For instance, you can pass locally
constructed tables to make private types or singletons.

Metamethods can be included in the methods argument along with normal methods. Standard
restrictions(e.g. those for __gc) still apply. The only non-standard logic would be a little
modification in NewIndexComposite that I added for some obscure property setter cases, but
that shouldn't trip you up if you don't return anything from __newindex calls.

Parameter table(optional fourth argument of class.define):

- The "base" field specifies the base type name.
- The "size" field will specify the size of the userdata, which is otherwise 0. Obviously this
  is more useful for types defined in C.
- The "bShared" field, if true, indicates that a class-wide table is used for the environment.
  So a table won't be created per-object, but I can still stash the type in the environment.




 
____________________________________________________________________________________
Yahoo! Music Unlimited
Access over 1 million songs.
http://music.yahoo.com/unlimited