lua-users home
lua-l archive

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


Mark Hamburg <mark@grubmah.com> dixit:

> The fundamentals of prototype-oriented, metatable-based design are probably that you should start out by thinking of your object as having three parts:
> 
> 	The primary table
> 	The metatable
> 	The __index table
> 
> More complex objects may have more pieces, but this works to start with conceptually.
> 
> The primary table supplies the object's identity and generally contains anything that varies on a per object basis. Generally when working this way, one assumes "encapsulation by convention" -- e.g., using a naming convention for private fields but doing nothing else to keep them private.

I guess this is ~ my model. Except that the primary table holds all what is specific to this clone line, including ordinary methods. This, to avoid lookup chains.

[Special details: An object intended as a prototype (i.e. to be cloned) holds all fields for an ordinary object (future clones), the metatable for this clone line, and what I call "generics" for the base mechanics (all which don't really belong to this clone line functionality, but simply allow clones to behave the way they should). "generics" is pointed by __index in the metatable.
Eg clone() itself and all metamethods will always reside inside generics, but Point's move() or color() won't. You can think at generics as something providing, in very simple manner, a feature in-between a class and a metaclass.
Clones won't get the "generics" field for economy (and also this would be disturbing in some cases, for instance if clones are collections), but they have a copy of the metatable, not only a pointer to it, see below.
As a consequence, a slot is either on the object itself, or inside its prototype's "generics" slot, as pointed by __index.]

> The metatable and the __index table supply shared behavior across a clone family -- i.e., the object cloned from a single prototype. The metatable supplies behaviors and fallbacks. The __index table supplies shared constant values and defaults for entries in the primary table. (I'm less fond of the trick of having the __index entry in the metatable point back to the metatable. As far as I can see, all this does is save one table in the system and it does so at the expense of making all of the metamethods available for __index access.)

Same for me. Except for the difference explained above.

> If you can design your object within this model, then the clone operation just needs to copy the primary table (shallow or deep cloning probably depends on the object and might benefit from a __clone metamethod) and copy the reference to the metatable.

Not only a reference, else it's not possible to adapt a clone's or a new prototype's metatable without changing the behaviour of ancestors. My intent is precisely to avoid complicated and useles lookup chain, but this must be done as well for the metatable.
The reason why I ask about the rationale behind the "metatable complication". Why not simply identify metamethods directly by name? (instead of them beeing indirectly referenced by metatable keys).

> If you want to make structural changes to the object to produce essentially a new clone family, you need to clone the metatable as well and possibly the __index table but generally this should be rare. This is the alternative to inheritance in a prototype-based system.

Right, and this not only applies to prototypes, but also to ordinary clones, if ever they would need to adapt. If metamethods were identified by name, we would not need to copy metatables.

For __index table (generics) changes, I can leave with a framework that requires doing them by hand. Eg writing:
Collection.generics.item = function (coll, key)
   return coll.data[key]
end
Precisely because in my logic such changes are very rare: generics beeing the very base mechanics of the object system, only when a totally different object line is defined is such a change required. For instance, the first File & the First Collection (maybe the first Sequence) will certainly demand changes to generics.

But the issue is different with metatable, for metamethods are much more "mondain" (lol) thingies that may change for each new prototype, and even on individual objects. See also below.

> One could also make changes to an existing object swapping out the appropriate pieces in the process to avoid stomping on other members of its clone family. For example, one could promote all of the fields in an object's primary table to the __index table resulting in a sequence like the following:
> 	
> 	-- Make a new copy
> 	local derived = base:clone()
> 	
> 	-- Make changes
> 	function derived:sayhello() print "I'm derived!" end
> 	
> 	--- Push changes from instance to shared space for easier and smaller future clones
> 	derived:updateindex()
> 
> On the other hand, this pattern doesn't work as well for changing metamethods. To deal with that, one probably wants a __newindex handler for the object that will clone and update the metatable in response to certain keys being set. (If metatables are sufficiently protected, it can probably avoid cloning the table if it has never been copied.)

Typically, a clone() (and/or prototype()) method can get as argument a table for new or overrided slots:
   Thing.generics.clone = function(thing, slots)
These new slots will complete or override base slots provided by the cloned object (what I call "thing" above). In numerous cases, this is enough to define a new object, even a new prototype, ready to work. (In mean there is not always a need for a real init() method.) But this friendly scheme is ruled out because of the metatable complication. If any provided slot happens to be a metamethod (eg the one that should be pointed by __tostring), overriding the method is not enough, unfortunately: we'll need to update the metatable, too.

The only solution I can think is establishing a set of standard metamethod names (that can well be the same as the keys); then the clone method can catch these new slot names to automatically update the metatable. (untested)
   mt = getmetatable(clone)
   for key,value in pairs(slots) do
       -- metanames is set of standard metamethod names
       if table.holds(metanames, key) do mt[key] = value end
   end
Obviously, this solution re-invents the simpler scheme of metamethods directly identified by name.

The alternative is to change the metamethod by hand:

Point = Object{
   <Point specific slot defs>
}
mt = getmetatable(Point)
mt.__tostring = <one of the new methods passed above>
mt.__add = <ditto>
mt.__mul = <ditto>

I find this useless burden. Again: why aren't metamethods identified by name?
The Problem here is that --as I said above, unlike changes to generics-- changes to the metatable are very frequent: nearly each prototype will require some, possibly given individual objects, too. (Fortunately, there is no __init!)

> Mark

Dein
________________________________

la vita e estrany

http://spir.wikidot.com/