ClosureClass

Showcase your libraries, tools and other projects that help your fellow love users.
User avatar
BlackBulletIV
Inner party member
Posts: 1261
Joined: Wed Dec 29, 2010 8:19 pm
Location: Queensland, Australia
Contact:

ClosureClass

Post by BlackBulletIV »

I found out about the closure approach to OOP in Lua, and I thought I'd give a shot at making a MiddleClass-like library using this. I don't have the time at this moment to write docs for it - but will shortly! - so for the meantime here's a small example:

Code: Select all

Foo =
class('Foo', SuperClass, function(self)
  local privateVar = 2
  
  local function hardcoreAction()
    -- bash, boom, bang!
  end 

  function self.instanceMethod(p1, p2)
    hardcoreAction()
    return p1 + p2
  end
  
  function self.getPrivateVar()
    return privateVar
  end
end)

function Foo.classOnlyFunction()
  print('foo!')
end

instance = Foo.new()
print(instance.instanceMethod(1, 2)) -- 3
print(instance.getPrivateVar()) -- 2
Foo.classOnlyFunction() -- "foo!"
It features a central Object class, mixins, and a few other things (like accessing subclasses via MyClass.subclasses[name]).

Here's the repo: https://github.com/BlackBulletIV/closureclass

EDIT: Oooh dear. Performance is bad for this library. It's about twice as slow as MiddleClass and uses a little over double the memory. I'll have to see what I can do about that. Anyway wanting to help improve the speed, please do.

By the way, MiddleClass is pretty jolly fast. It can create 2,000,000 instances of two different classes (1m for each class) in 3.8 seconds. Not bad.

EDIT 2: Hang on. Accessing and calling methods in ClosureClass takes about 4/5 the speed of MiddleClass.
User avatar
EmmanuelOga
Citizen
Posts: 56
Joined: Thu Apr 22, 2010 9:42 pm
Location: Buenos Aires, Argentina
Contact:

Re: ClosureClass

Post by EmmanuelOga »

Hey, thanks for sharing this. In my case I like to use closures for building objects in a more raw style.You don't really need full blown classes while working with a prototypical language like lua.

The two guidelines I follow are:

* Choose composition over inheritance
* Does that really need to be private?

When using inheritance with the closure approach, I simply create a second factory function to instantiate the 'superclass' and then modify it to my heart content. Here is an example of Square-is-a-Shape and so on.

http://pastie.org/1659847

In regard to speed/memory consumption, in general using closures for building objects may take a little more memory as you may be creating more stuff in each instantiation (methods are new functions for every object). The advantage is that lots of stuff can be saved in the object closure, which is often faster for lua to access.

I would not mind a lot about 'speed' though. The differences in the synthetic benchmark in the article you mention are 47secs versus 38secs for 1.000.000 iterations and 48kb versus 61Kb for 100.000 objects. It does not seem like it would affect a lot most 'normal', 30/60FPS games. I want to think most games won't be creating and getting rid of even 10.000 objects per frame (if you game does... you really have a problem with either approach :death: ) .

The main advantage of this approach is its 'cleanness' (subjective, I know), and that you don't really need any boilerplate code to use it.
Last edited by EmmanuelOga on Fri Mar 11, 2011 7:04 pm, edited 1 time in total.
--------------------------------------------------------------------------------------------------------
http://EmmanuelOga.com
User avatar
crow
Party member
Posts: 186
Joined: Thu Feb 24, 2011 11:47 pm
Location: UK
Contact:

Re: ClosureClass

Post by crow »

How come you limited you functions to the local scope, just interested thats all :)
Sir Kittenface
Möko IDE Codename (Erös) Returns Soon

I am dyslexic so if any of my replys confusing please just ask me to reword it as this will make things a lot easier for all parties lol.
User avatar
nevon
Commander of the Circuloids
Posts: 938
Joined: Thu Feb 14, 2008 8:25 pm
Location: Stockholm, Sweden
Contact:

Re: ClosureClass

Post by nevon »

crow wrote:How come you limited you functions to the local scope, just interested thats all :)
Because otherwise they'd be global, rather than encapsulated into an object.
User avatar
EmmanuelOga
Citizen
Posts: 56
Joined: Thu Apr 22, 2010 9:42 pm
Location: Buenos Aires, Argentina
Contact:

Re: ClosureClass

Post by EmmanuelOga »

nevon wrote:
crow wrote:How come you limited you functions to the local scope, just interested thats all :)
Because otherwise they'd be global, rather than encapsulated into an object.
Right, top-level local functions inside a given file are useful to keep those functions local to the scope of that file. If you want to share a couple of functions across other files you can do something like this:

Code: Select all

  -- file1
  local function a() ... end
  local function b() ... end
  local function c() ... end
 
  return {cool = a, cooler=b} -- do not share c.

Code: Select all

  -- file2
  local function a() ... end
  local function b() ... end
   return {cool = a, cooler=b}

Code: Select all

  -- main program
  local thing1 = require('file1')
  local thing2 = require('file2')   
  print(thing1.cool(), thing2.cool())
This is useful to avoid globals stepping over each other. Also, notice we now can access two different functions named the same way ('cool'), since we have each file function under its own "namespace" (the table that contains the reference to those functions).
--------------------------------------------------------------------------------------------------------
http://EmmanuelOga.com
User avatar
crow
Party member
Posts: 186
Joined: Thu Feb 24, 2011 11:47 pm
Location: UK
Contact:

Re: ClosureClass

Post by crow »

Hmmm, I guess because I prebuilt all my functions before hand i never had to do it like that, I have to read up on it more at some point :) thanks :megagrin:
Sir Kittenface
Möko IDE Codename (Erös) Returns Soon

I am dyslexic so if any of my replys confusing please just ask me to reword it as this will make things a lot easier for all parties lol.
User avatar
kikito
Inner party member
Posts: 3153
Joined: Sat Oct 03, 2009 5:22 pm
Location: Madrid, Spain
Contact:

Re: ClosureClass

Post by kikito »

The idea of using a function scope is very nice. It didn't occur to me.
BlackBulletIV wrote: By the way, MiddleClass is pretty jolly fast. It can create 2,000,000 instances of two different classes (1m for each class) in 3.8 seconds. Not bad.

EDIT 2: Hang on. Accessing and calling methods in ClosureClass takes about 4/5 the speed of MiddleClass.
Can you show us the tests you are using?

Regarding speed - on middleclass, the more "deep" your method is on the class hierarchy, the more expensive it is to use it. More classes have to be "traversed".

Code: Select all

 A = class('A')
function A:foo() print('foo') end
B = class('B', A)
C = class('C', B)
a = A:new()
c = C:new()
-- a:foo() is faster than c:foo(), because there are 2 "indirections" less
That is were middleclass should perform the worst. I see that you are copying the module functions & class members when creating a subclass, as well as creating new implementations of new and include. If I'm reading that correctly, that should take care of the indirections, at the cost of taking more memory, and some flexibility - since there are no indirections, if you change the superclass, the subclasses don't notice it.

Code: Select all

A = class('A')
function A:foo() print('foo') end
B = class('B', A)
-- we change A again
function A:foo() print('bar') end 
a = A:new()
b = B:new()
a:foo()
b:foo()
On middleclass that should print 'bar', 'bar'. I think on ClosureClass it might print 'bar', 'foo' (can't test it now). This is not very important unless you want to be able to do monkeypatching on classes.

It should be possible to increase speed even further by copying the methods directly to the instances. But that would consume even more memory*. By the way, I see that right now metamethods are copied into each instance. Is that intended?

Sorry if I didn't understand everything correctly and I talked nonsense.


*: Middleclass is called middleclass because it's intended to be the "middle point" between that (copying all methods to each instance, gaining speed but losing flexibility) and having a full-fledged Object Model (where you have a Class class, and classes are instances of it, like in ruby).
When I write def I mean function.
User avatar
BlackBulletIV
Inner party member
Posts: 1261
Joined: Wed Dec 29, 2010 8:19 pm
Location: Queensland, Australia
Contact:

Re: ClosureClass

Post by BlackBulletIV »

EmmanuelOga wrote:Hey, thanks for sharing this. In my case I like to use closures for building objects in a more raw style.You don't really need full blown classes while working with a prototypical language like lua.

The two guidelines I follow are:

* Choose composition over inheritance
* Does that really need to be private?

When using inheritance with the closure approach, I simply create a second factory function to instantiate the 'superclass' and then modify it to my heart content. Here is an example of Square-is-a-Shape and so on.

http://pastie.org/1659847

In regard to speed/memory consumption, in general using closures for building objects may take a little more memory as you may be creating more stuff in each instantiation (methods are new functions for every object). The advantage is that lots of stuff can be saved in the object closure, which is often faster for lua to access.

I would not mind a lot about 'speed' though. The differences in the synthetic benchmark in the article you mention are 47secs versus 38secs for 1.000.000 iterations and 48kb versus 61Kb for 100.000 objects. It does not seem like it would affect a lot most 'normal', 30/60FPS games. I want to think most games won't be creating and getting rid of even 10.000 objects per frame (if you game does... you really have a problem with either approach :death: ) .

The main advantage of this approach is its 'cleanness' (subjective, I know), and that you don't really need any boilerplate code to use it.
You don't really need a full blown class system, but I love having one (I'm an OOP nut-head of sorts).

The funny thing is that ClosureClass gets even worse speed and memory consumption. Here's the test code I'm running:

Code: Select all

args = {...}

if args[2] == 'memory' then
  if args[1] == "middleclass" then
    require('middleclass.init')
    A = class('A')
    B = class('B', A)
    t = {}

    function A:method()
      return 1 + 1
    end

    function B:b()
      return 2 + 2
    end

    for i = 1, 1000000 do
      table.insert(t, A:new())
      table.insert(t, B:new())
    end
  else
    require('closureclass.init')
    t = {}
    
    A = class('A', function(self)
      function self.method()
        return 1 + 1
      end
    end)

    B = class('B', A, function(self)
      function self.b()
        return 2 + 2
      end
    end)

    for i = 1, 1000000 do
      table.insert(t, A.new())
      table.insert(t, B.new())
    end
  end
  
  print("Memory in use: " .. collectgarbage("count") .. " KB")
else
  if args[1] == "middleclass" then
    require('middleclass.init')
    A = class('A')
    B = class('B', A)
  
    function A:method()
      return 1 + 1
    end
  
    function B:b()
      return 2 + 2
    end
    
    instance = A()
    
    for i = 1, 10000000 do
      A:method()
    end
  else
    require('closureclass.init')
    A = class('A', function(self)
      function self.method()
        return 1 + 1
      end
    end)
  
    B = class('B', A, function(self)
      function self.b()
        return 2 + 2
      end
    end)
    
    instance = A.new()
  
    for i = 1, 10000000 do
      instance.method()
    end
  end
end
Results:

Code: Select all

$ time lua test_oop.lua middleclass memory
Memory in use: 235941.64257812 KB

real	0m3.822s
user	0m3.479s
sys	0m0.334s
$ time lua test_oop.lua closureclass memory
Memory in use: 509374.98046875 KB

real	0m8.059s
user	0m7.383s
sys	0m0.668s
Not very good.

EDIT: Oh yeah, my computer is Mac Pro 1,1 running two Intel Xeon 2.66 Ghz Dual Cores.

Indeed, I think, that closures look a lot nicer, when declaring classes and calling methods. The syntax shown for ClosureClass above really appeals to me, in fact I was even going to try to achieve this:

Code: Select all

class('Name' < SuperClass, initFunc)
But Lua wouldn't allow me to redefine string's metatable's __lt method.
kikito wrote:The idea of using a function scope is very nice. It didn't occur to me.
BlackBulletIV wrote: By the way, MiddleClass is pretty jolly fast. It can create 2,000,000 instances of two different classes (1m for each class) in 3.8 seconds. Not bad.

EDIT 2: Hang on. Accessing and calling methods in ClosureClass takes about 4/5 the speed of MiddleClass.
Can you show us the tests you are using?

Regarding speed - on middleclass, the more "deep" your method is on the class hierarchy, the more expensive it is to use it. More classes have to be "traversed".

Code: Select all

 A = class('A')
function A:foo() print('foo') end
B = class('B', A)
C = class('C', B)
a = A:new()
c = C:new()
-- a:foo() is faster than c:foo(), because there are 2 "indirections" less
That is were middleclass should perform the worst. I see that you are copying the module functions & class members when creating a subclass, as well as creating new implementations of new and include. If I'm reading that correctly, that should take care of the indirections, at the cost of taking more memory, and some flexibility - since there are no indirections, if you change the superclass, the subclasses don't notice it.

Code: Select all

A = class('A')
function A:foo() print('foo') end
B = class('B', A)
-- we change A again
function A:foo() print('bar') end 
a = A:new()
b = B:new()
a:foo()
b:foo()
On middleclass that should print 'bar', 'bar'. I think on ClosureClass it might print 'bar', 'foo' (can't test it now). This is not very important unless you want to be able to do monkeypatching on classes.

It should be possible to increase speed even further by copying the methods directly to the instances. But that would consume even more memory*. By the way, I see that right now metamethods are copied into each instance. Is that intended?

Sorry if I didn't understand everything correctly and I talked nonsense.


*: Middleclass is called middleclass because it's intended to be the "middle point" between that (copying all methods to each instance, gaining speed but losing flexibility) and having a full-fledged Object Model (where you have a Class class, and classes are instances of it, like in ruby).
Indeed, this is one big problem with closures, instances won't respond if you modify the class, which is a shame.

I'm not sure what you're talking about by copying methods directly to instances. If you're talking about the code the copies stuff from superclass to subclass, that's for inheriting class methods and attributes (and also modules).

I'm guessing metamethods are copied, I'm not quite sure how it works in MiddleClass (don't you just define an instance function). Anyway, metamethods are yet to be handled.

Thanks for the feedback guys!
User avatar
kikito
Inner party member
Posts: 3153
Joined: Sat Oct 03, 2009 5:22 pm
Location: Madrid, Spain
Contact:

Re: ClosureClass

Post by kikito »

BlackBulletIV wrote:Indeed, this is one big problem with closures, instances won't respond if you modify the class, which is a shame.
Are you sure this is a problem due to closure usage? My gut tells me there must be a way to implement indirection with them too.
BlackBulletIV wrote:I'm not sure what you're talking about by copying methods directly to instances. If you're talking about the code the copies stuff from superclass to subclass, that's for inheriting class methods and attributes (and also modules).
I was talking about this code inside _new (pasting below for reference)

Code: Select all

for _, m in pairs(cls.__modules) do
    for name, func in pairs(m) do
      if name ~= 'included' then
        self[name] = function(...) return func(self, ...) end -- get rid of self for the outside world
      end
    end
  end
I'm not sure of what it does - maybe getting rid of the need of using : instead of . ? But then, why doing that only with module methods?
BlackBulletIV wrote:I'm guessing metamethods are copied, I'm not quite sure how it works in MiddleClass (don't you just define an instance function). Anyway, metamethods are yet to be handled.
Metamethods are a bit of a pain on lua when dealing with inheritance. The problem is that Lua doesn't want to use metatables in order to get metamethods: a table must have a __tostring metamethod on its metatable; if its "parent" (the metatable pointed by __index) has a __tostring, Lua doesn't care.

On middleclass I handle this by creating a set of "default metamethods" on every new class. These methods "point" to the methods on the superclass. And if there's no parent, they throw an error, just like a regular method would.
When I write def I mean function.
User avatar
Robin
The Omniscient
Posts: 6506
Joined: Fri Feb 20, 2009 4:29 pm
Location: The Netherlands
Contact:

Re: ClosureClass

Post by Robin »

kikito wrote:Metamethods are a bit of a pain on lua when dealing with inheritance. The problem is that Lua doesn't want to use metatables in order to get metamethods: a table must have a __tostring metamethod on its metatable; if its "parent" (the metatable pointed by __index) has a __tostring, Lua doesn't care.
That is because

Code: Select all

mt.__index = t -- where mt is the metatable of some other table and t is yet another table
is really a shortcut for

Code: Select all

mt.__index = function (table, key) return t[key] end
Thus t is really not a "parent" of whatever uses mt as metatable, it's just a place to get values for missing keys from.
Help us help you: attach a .love.
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot] and 4 guests