lights FAQ Forum github.com/luapower/oo
oo

Standard Libraries
glue
pp
oo
events WIP
lpeg
coro

oo

Fancy object system


local oo = require'oo'

Object system with virtual properties and method overriding hooks.

In a nutshell

  • single, dynamic inheritance by default:
    • Fruit = oo.Fruit()
    • Apple = oo.Apple(Fruit, {carpels = 5})
    • Apple.carpels -> 5 (class field)
    • apple = Apple(...)
    • apple.carpels -> 5 (serves as default value)
    • apple.super -> Apple
    • Apple.super -> Fruit
    • apple.isApple, apple.isFruit, Apple.isApple, Apple.isFruit -> true
  • multiple, static inheritance by request:
    • Apple:inherit(Fruit[,replace]) - statically inherit Fruit, optionally replacing existing properties.
    • Apple:detach() - detach from the parent class, in other words statically inherit self.super.
  • virtual properties with getter and setter:
    • reading apple.foo calls Apple:get_foo() to get the value, if apple.get_foo is defined.
    • assignment to apple.foo calls Apple:set_foo(value) if Apple.set_foo is defined.
    • missing the setter, the property is considered read-only and the assignment fails.
  • method overriding hooks:
    • function Apple:before_pick(args...) end makes apple:pick() call the code inside before_pick() first.
    • function Apple:after_pick(args...) end makes apple:pick() call the code inside after_pick() last.
    • function Apple:override_pick(inherited, ...) lets you override Apple:pick() and call inherited(self, ...).
  • virtual classes: nested inner classes which can overriden in subclasses of the outer class (more below).
  • introspection:
    • oo.is(obj|class, class|classname) -> true|false - check instance/class ancestry
    • oo.isinstance(obj|class[, class|classname]) -> true|false - check instance ancestry
    • oo.issubclass(class[, class|classname]) -> true|false - check class ancestry
    • apple:is(class|classname) -> true|false - check instance/class ancestry
    • apple:isinstance([class|classname]) -> true|false - check instance ancestry
    • Apple:issubclass([class|classname]) -> true|false - check class ancestry
    • apple:closest_ancestor(orange) -> Fruit - closest ancestor of other in self's hierarchy
    • `apple:hasproperty(name) -> false | true, 'field'|'property' - check if property exists without accessing its value
    • self:allpairs([super]) -> iterator() -> name, value, source - iterate all properties, including inherited and overriden ones up until super.
    • self:properties([super]) -> get a table of all current properties and values, including inherited ones up until super.
    • self:inspect([show_oo_fields]) - inspect the class/instance structure and contents in detail (requires glue).
  • overridable subclassing and instantiation mechanisms:
    • Fruit = oo.Fruit() is sugar for Fruit = oo.Object:subclass('Fruit')
    • Apple = oo.Apple(Fruit) is sugar for Apple = Fruit:subclass('Apple')
    • apple = Apple(...) is sugar for apple = Apple:create(...)
      • Apple:create() calls apple:init(...)

Inheritance and instantiation

Classes are created with oo.ClassName([super]), where super is usually another class, but can also be an instance, which is useful for creating polymorphic "views" on existing instances.

local Fruit = oo.Fruit()

You can also create anonymous classes with oo.class([super]):

local cls = oo.class()

Instances are created with cls:create(...) or simply cls(), which in turn calls cls:init(...) which is the object constructor. While cls is normally a class, it can also be an instance, which effectively enables prototype-based inheritance.

local obj = cls()

The superclass of a class or the class of an instance is accessible as self.super.

assert(obj.super == cls)
assert(cls.super == oo.Object)

Inheritance is dynamic: properties are looked up at runtime in self.super and changing a property or method in the superclass reflects on all subclasses and instances. This can be slow, but it saves space.

cls.the_answer = 42
assert(obj.the_answer == 42)

You can detach the class/instance from its parent class by calling self:detach() -> self. This copies all inherited fields to the class/instance and removes self.super.

cls:detach()
obj:detach()
assert(obj.super == nil)
assert(cls.super == nil)
assert(cls.the_answer == 42)
assert(obj.the_answer == 42)

Static inheritance can be achieved by calling self:inherit([other],[replace],[stop_super]) -> self which copies over the properties of another class or instance, effectively monkey-patching self, optionally overriding properties with the same name. The fields self.classname and self.super are always preserved though, even with the override flag.

  • other can also be a plain table, in which case it is shallow-copied.
  • other defaults to self.super.
  • stop_super limits how far up in the inheritance chain of other too look for fields and properties to copy.
  • if other is not in self's hierarchy, stop_super defaults to self:closest_ancestor(other) in order to prevent inheriting any fields from common ancestors, which would undo any overridings done in subclasses of the closest ancestor.
local other_cls = oo.class()
other_cls.the_answer = 13

obj:inherit(other_cls)
assert(obj.the_answer == 13) --obj continues to dynamically inherit cls.the_answer
                             --but statically inherited other_cls.the_answer

obj.the_answer = nil
assert(obj.the_answer == 42) --reverted to class default

cls:inherit(other_cls)
assert(cls.the_answer == 42) --no override

cls:inherit(other_cls, true)
assert(cls.the_answer == 13) --override

In fact, self:detach() is written as self:inherit(self.super) with the minor detail of setting self.classname = self.classname and removing self.super.

NOTE: Detaching instances or final classes helps preventing LuaJIT from bailing out to the interpreter which can result in 100x performance drop. Even in interpreter mode, detaching instances can increase performance for method lookup by 10x (see benchmarks).

You can do this easily with:

--detach instances of (subclasses of) myclass from their class.
--patching myclass or its subclasses afterwards will not affect
--existing instances but it will affect new instnaces.
function myclass:before_init()
   self:detach()
end

--detach all new subclasses of myclass. patching myclass or its
--supers afterwards will have no effect on existing subclasses
--of myclass or its instances. patching final classes though
--will affect both new and existing instances.
function myclass:override_subclass(inherited, ...)
   return inherited(self, ...):detach()
end

NOTE: Static inheritance changes field lookup semantics in a subtle way: because field values no longer dynamically overshadow the values set in the superclasses, setting a statically inherited field to nil doesn't expose back the value from the super class, instead the field remains nil.

To further customize how the values are copied over for static inheritance, override self:properties().

Virtual properties

Virtual properties are created by defining a getter and a setter. Once you have defined self:get_foo() and self:set_foo(value) you can read and write to self.foo and the getter and setter will be called to fulfill the indexing. The setter is optional. Assigning a value to a property without a setter removes the getter and sets the value. This can be used for implementing autoloading of component classes (see glue.autoload).

function cls:get_answer_to_life() return deep_thought:get_answer() end
function cls:set_answer_to_life(v) deep_thought:set_answer(v) end
obj = cls()
obj.answer_to_life = 42
assert(obj.answer_to_life == 42) --assuming deep_thought can store a number

Virtual properties can be generated in bulk given a multikey getter and a multikey setter and a list of property names, by calling self:gen_properties(names, [getter], [setter]). The setter and getter must be methods of form:

  • getter(self, k) -> v
  • setter(self, k, v)

Overriding hooks

Overriding hooks are sugar to make method overriding more easy and readable.

Instead of:

function Apple:pick(arg)
   print('picking', arg)
   local ret = Apple.super.pick(self, arg)
   print('picked', ret)
   return ret
end

Write:

function Apple:override_pick(inherited, arg, ...)
   print('picking', arg)
   local ret = inherited(self, arg, ...)
   print('picked', ret)
   return ret
end

Or even better:

function Apple:before_pick(arg)
   print('picking', arg)
end

function Apple:after_pick(arg)
   print('picked', arg)
   return ret
end

By defining self:before_<method>(...) a new implementation for self.<method> is created which calls the before hook and then calls the existing (inherited) implementation. Both calls receive all arguments.

By defining self:after_<method>(...) a new implementation for self.<method> is created which calls the existing (inherited) implementation, after which it calls the hook and returns whatever the hook returns. Both calls receive all arguments.

By defining self:override_<method>(inherited, ...) you can access self.super.<method> as inherited.

function cls:before_init(foo, bar)
  self.foo = foo or default_foo
  self.bar = bar or default_bar
end

function cls:after_init()
  --allocate resources
end

function cls:before_destroy()
  --destroy resources
end

If you don't know the name of the method you want to override until runtime, use cls:before(name, func), cls:after(name, func) and cls:override(name, func) instead.

Virtual classes

Virtual classes provide an additional way to extend composite objects (objects which need to instantiate other objects) beyond inheritance which doesn't by itself cover extending the classes of the sub-objects of the composite object. Virtual classes come for free in languages where classes are first-class entitites: all you have to do is to make the inner class a class field of the outer class and instantiate it with self:inner_class(). This simple indirection has many advantages:

  • it allows subclassing the inner class in subclasses of the outer class by just overriding the inner_class field.
  • using self:inner_class() instead of self.inner_class() passes the outer object as the second arg to the constructor of the inner object (the first arg is the inner object) so that you can reference the outer object in the constructor, which is usually needed.
  • theinner_class field can be used as a method of the outer class so it can be made part of its public API without needing any additional wrapping, and it can also be overriden with a normal method in subclasses of the outer class (the overriding mechanism still works even if it's not overriding a real method).

Events

Events are useful for associating actions with callback functions. This can already be done more flexibly with plain methods and overriding, but events have the distinct ability to revert the overidding at runtime (with obj:off()). They also differ in the fact that returning a non-nil value from a callback short-circuits the call chain and the value is returned back to the user.

The events functionality can be enabled by adding the events mixin to oo's base class (or to any other class):

local events = require'events'
oo.Object:inherit(events)

Performance Tips

Instance fields are accessed directly but methods and default values (class fields) go through a slower dynamic dispatch function (it's the price you pay for virtual properties). Copying class fields to the instance by calling self:inherit() will short-circuit this lookup at the expense of more memory consumption. Fields with a nil value go through the same function too so providing a false default value to those fields will also speed up their lookup.


Last updated: 2 days ago | Edit on GitHub

Pkg type:Lua
Version: r12-4-g6c18224
Last commit:
License: Public Domain
Requires: none
Required by: ui 

Top