Some ways to get encapsulation in Lua

classic Classic list List threaded Threaded
7 messages Options
Reply | Threaded
Open this post in threaded view
|

Some ways to get encapsulation in Lua

Mark Hamburg-4
The following approaches to data encapsulation will work in the stock Lua
distribution. By encapsulation, I assume one wants to hand out a data object
and not have clients be able to interact with it except through method calls
or other published properties.


Function closures
-----------------
This is covered in Programming in Lua. Since a function can reference
upvalues and clients can't look inside without using the debug interface --
and in any context where encapsulation mattered, one should probably remove
the debug interface. The chief downside is that construction and usage don't
have much in common with other approaches to implementing objects in Lua.

    function makeAccount( bankID, balance )
        local self = function( msg, ... )
            if msg == "balance" then
                return balance
            elseif msg == "deposit" then
                local amount = select( 1, ... )
                assert( 0 <= amount )
                balance = balance + amount
            elseif msg == "withdraw" then
                local amount = select( 1, ... )
                assert( 0 <= amount )
                assert( amount <= balance )
                balance = balance - amount
            elseif msg == "payInterest" then
                local rate = select( 1, ... )
                assert( 0 <= rate )
                local interest = balance * rate
                assert( bankID == select( 2, ... )
                self( "deposit", interest )
            end
        end
        return self
    end

Usage is:

    account = makeAccount( secretBankID, 10000 )
    account( "withdraw", 500 )
    account( "payInterest", 0.05, secretBankID )
    account( "payInterest", 0.01, "scam" ) --> asserts out

The one change to Lua that would add sugar for this would be to specify that
obj:msg( ... ) translates to obj( "msg", ... ) if obj is a function. That's
just a change to OP_SELF.

Without this change, if one wants to make function-closure based
encapsulation look like other objects one needs to create a table containing
a reference to the function and have method declarations for the table that
do the appropriate conversion. For example:

    local AccountMeta = {}
    AccountMeta.__index = AccountMeta
    function AccountMeta:deposit( ... )
        return self.rep( "deposit", ... )
    end
    function AccountMeta:withdraw( ... )
        return self.rep( "withdraw", ... )
    end
    function AccountMeta:payInterest( ... )
        return self.rep( "payInterest", ... )
    end
    
    local function wrapAccountFunc( func )
        return setmetatable( { rep = func }, AccountMeta )
    end

Note that these functions could be generated on demand if we assumed that
all field access was to get methods.


Proxy tables with hidden representations
----------------------------------------
The basic idea here is that we hand out proxy tables while keeping the
representations hidden away. This requires a somewhat complicated set of
table relationships to implement:

1. The proxy needs to have a metatable that is unique to the proxy, is
protected from access by Lua code, and that references the representation.
This reference to the rep is necessary to make the link from proxy to
representation strong. We put it in the metatable so that we can hide it.

2. We need a fully-weak table mapping proxies to representations. This is
how a method invoked on the proxy will find it's representation since the
metatable protection will keep even code inside the encapsulation boundary
from getting the metatable. This table needs to be fully-weak and hence we
need the link in the metatable because just using a weak-keyed table and no
metatable link could fall victim to the cyclic issues with respect to weak
tables.

3. It is useful to maintain a fully-weak table mapping representations to
proxies so that we only create one proxy per representation. This could also
be done by having a link in the representation to its proxy.

Converting a representation into a proxy looks something like the following:

    local gRep2Proxy = setmetatable( {}, { __mode = "kv" } )
    local gProxy2Rep = setmetatable( {}, { __mode = "kv" } )

    function rep2proxy( rep )
        local proxy = gRep2Proxy[ rep ]
        if proxy then return proxy end
        local mt = {}
        mt.__metatable = "can't look inside me"
        mt.rep = rep
        mt.__index = < index function >
        mt.__newindex = < newindex function >
        proxy = setmetatable( {}, mt )
        gRep2Proxy[ rep ] = proxy
        gProxy2Rep[ proxy ] = rep
    end

We could probably standardize __index and __newindex functions in the proxy
so that they did the appropriate thing with respect to the encapsulated
object.

One could also build a system this way in which the reps themselves had the
necessary entries to be metatables for the proxies.

One might even be able to use function closures to eliminate the
proxy-to-rep table but this would come at the expense of instantiating even
more objects per usage.


Non-stock Lua
-------------
If we are prepared to change the Lua library, but not the VM or language,
then we can get encapsulation via private keys in tables. Private keys are
easy to generate: one just creates a table.


    local key_balance = {}
    local key_bankID = {}

    local AccountMeta = {}

    AccountMeta.__index = AccountMeta

    function AccountMeta:deposit( amount )
        assert( 0 <= amount )
        self[ key_balance ] = self[ key_balance ] + amount
    end

    function AccountMeta:withdraw( amount )
        assert( 0 <= amount )
        assert( amount <= self[ key_balance ] )
        self[ key_balance ] = self[ key_balance ] - amount
    end

    etc.

    function makeAccount( bankID, balance )
        return setmetatable( {
                [ key_balance ] = balance,
                [ key_bankID ] = bankID
            },
            AccountMeta )
    end

The catch is that these fields aren't really private as implemented since if
client code could obtain the keys, client code could access the fields
directly. How could client code obtain the keys? By iterating over the
table. So, we need a way to keep next and pairs from working on the table.
This could be done if pairs and next looked for metatable entries before
defaulting to the raw operations. The raw operations also have to be
inaccessible -- i.e., there can be no rawnext in the standard library.


Non-stock Lua 2: Less draconian metatable protection
----------------------------------------------------
If we could override metatable protection perhaps by allowing getmetatable
to take extra parameters it would pass to an unlock function in the
metatable, we could eliminate the weak tables in the proxy case. For
example, we could define getmetatable as follows using a hypothetical
rawgetmetatable:

    function getmetatable( t, ... )
        local mt = rawgetmetatable( t )
        if mt then
            local protect = mt.__metatable
            if type( protect ) == "function" then
                return protect( t, mt, ... )
            else
                return protect
            end
        end
        return mt
    end


Deeper Lua changes: Tables as their own proxies
-----------------------------------------------
We could reduce overhead in the proxy case if tables could be their own
proxies. A table that was its own proxy would need to be able to take two
forms as a TValue. In one form, it would be a table and would be just as
accessible as any other table. In another form, it would be marked as a
proxy and all access would go through metamethods much as if the table were
a userdata. I don't think this would actually be that hard to implement in
the VM. The consistency macro that makes sure the type in a TValue matches
the type in the heap-allocated struct itself would have to cope with
proxies, but that only generates code when the Lua core is built with
consistency checks active.

The extensions to the language spec would essentially be:

1. A new type value "proxy"

2. A new function toproxy which takes a table and returns the table as a
proxy.

Client code could use fully-weak tables to map back the other way or the
library could provide a metatable mechanism for proxies where the proxy's
metatable would provide a function determining whether to allow access to
the table based on additional parameters passed (e.g., an identity key). For
example:

    function mt:__fromproxy( proxy, table, key )
        if key == mySecretKey then
            return table
        else
            return nil
        end

This would be called by a library function fromproxy that would take a proxy
plus extra parameters to pass to the __fromproxy metamethod.
   
Mark


Reply | Threaded
Open this post in threaded view
|

Re: Some ways to get encapsulation in Lua

Petite Abeille

On Jan 25, 2005, at 19:49, Mark Hamburg wrote:

Function closures
-----------------
This is covered in Programming in Lua. Since a function can reference
upvalues and clients can't look inside without using the debug interface -- and in any context where encapsulation mattered, one should probably remove the debug interface. The chief downside is that construction and usage don't have much in common with other approaches to implementing objects in Lua.

One thing that I like with this approach is that, beside providing encapsulation, it provides, er, "visual encapsulation". In other words, the entire object is defined inside one function. This is pretty neat :)

For example:

// Root class

function LUObject()
    local self = {}

    local description = function()
	    print "default description method"
    end

    return
    {
	    description = description
    }, self
end

// A subclass

function MySubclass()
    local self, super = LUObject();

    local description = function()
	    print "my very own description as well"
	    super.description()
    end

    return
    {
	    description = description
    }, self
end

And finally:

anObject = MySubclass()
anObject.description()

As an added deviance, if you wanted to make a class <gasp> "final" </gasp>, you would not return "self" at the end of an "object" function... scary thought :o)

Would the above scheme provide both inheritance and encapsulation without too much fuss?

Cheers

--
PA
http://alt.textdrive.com/


Reply | Threaded
Open this post in threaded view
|

Re: Some ways to get encapsulation in Lua

Jamie Webb-3
In reply to this post by Mark Hamburg-4
Here's one I don't recall seeing mentioned in this discussion:

"Object Inversion"
------------------
This is another way to store the private attributes in function
closures. We avoid creating new closures for each object by using
attribute tables indexed by self (which remains empty unless there are
public attributes).

module "Rectangle"

-- Tables which hold attributes
local weak = { __mode = "k" }
local width = setmetatable({}, weak)
local height = setmetatable({}, weak)

-- The table of methods
local methods = {}
local meta = { __index = methods }

-- The constructor
function new(w, h)
	local self = {}
	width[self] = w
	height[self] = h
	return setmetatable(self, meta)
end

-- A method which uses the private attributes
function methods:area()
	return width[self] * height[self]
end

-- Inheritance function
function subclass()
	return { width = width, height = height, methods = methods }
end


PROS:
- Good privacy
- Uses standard method call notation
- Uses fairly nice class definition notation
- Possible to have public attributes also
- Time/space overheads roughly the same as 'classic' objects
- Can choose whether each attribute is private or protected
- Unlike straight closures, time/space performance doesn't degrade for
  large numbers of methods

CONS:
- Uses weak tables, so there is a danger of uncollectable reference
  cycles (but the Python folks don't seem too distraught about that)
- Not as fast as straight closure objects for attribute access
- Uses non-standard syntax for attribute access
- Have to explicitly permit inheritance
- Any more? I've never actually tried this method...

-- Jamie Webb

Reply | Threaded
Open this post in threaded view
|

Re: Some ways to get encapsulation in Lua

Mark Hamburg-4
In reply to this post by Petite Abeille
on 1/25/05 11:53 AM, PA at [hidden email] wrote:

> For example:
> 
> // Root class
> 
> function LUObject()
>    local self = {}
> 
>    local description = function()
>    print "default description method"
>    end
> 
>    return
>    {
>    description = description
>    }, self
> end
> [ and more ]
> 
> Would the above scheme provide both inheritance and encapsulation
> without too much fuss?

Yes, but at a fairly high cost per object since every method has to be
instantiated and stored into the public table.

Mark


Reply | Threaded
Open this post in threaded view
|

Re: Some ways to get encapsulation in Lua

Adrian Sietsma
In reply to this post by Petite Abeille
PA wrote:
One thing that I like with this approach is that, beside providing encapsulation, it provides, er, "visual encapsulation". In other words, the entire object is defined inside one function. This is pretty neat :)
...
anObject = MySubclass()
anObject.description()

that is *very* neat. i have a conceptual version using a 'class' userdata, where you would say

require "class"
local animal = class{ table describing class : functions and methods}
local cat = animal{local attribs etc}

simply because userdata functions always call __index, so read-only is the default.

but your way is better.

Adrian

Reply | Threaded
Open this post in threaded view
|

Re: Some ways to get encapsulation in Lua

Petite Abeille
In reply to this post by Mark Hamburg-4

On Jan 26, 2005, at 03:16, Mark Hamburg wrote:

Would the above scheme provide both inheritance and encapsulation
without too much fuss?

Yes, but at a fairly high cost per object since every method has to be
instantiated and stored into the public table.

Hmmm... I thought that "instantiation" was one of the tenet of OOP?

But as long as the price is right...

Cheers

--
PA
http://alt.textdrive.com/


Reply | Threaded
Open this post in threaded view
|

Re: Some ways to get encapsulation in Lua

Mark Hamburg-4
on 1/26/05 3:12 AM, PA at [hidden email] wrote:

> 
> On Jan 26, 2005, at 03:16, Mark Hamburg wrote:
> 
>>> Would the above scheme provide both inheritance and encapsulation
>>> without too much fuss?
>> 
>> Yes, but at a fairly high cost per object since every method has to be
>> instantiated and stored into the public table.
> 
> Hmmm... I thought that "instantiation" was one of the tenet of OOP?
> 
> But as long as the price is right...

Well, yes, instantiation is a basic tenet of OOP, but there are multiple
ways to implement things and instantiating every method of an instance is
expensive in terms of both memory and time to construct an object. It does,
however, lead to objects that run fast since there is no __index table
lookup when looking for methods and using closure variables can avoid other
table lookups (again at an expense in memory usage).

Mark