This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
GravityScore's profile picture

Metatables/oop Help

Started by GravityScore, 25 August 2013 - 11:25 AM
GravityScore #1
Posted 25 August 2013 - 01:25 PM
Hey,

I'm stuck on a problem to do with OOP and metatables. The code examples below are bare bones sort of things.

I've got a base class:

BaseObject = {
tag = "",
new = function(self)
  local new = {}
  setmetatable(new, {__index = self})
  return new
end,
_draw = function(self) end,
_event = function(self, event, a, b, c) end,
}

And 2 classes that inherit from it:

Label = {
__index = BaseObject,

--!! important bit:
textColor = colors.white,
backgroundColor = colors.black,
--!!

new = function(self)
  local new = {}
  setmetatable(new, {__index = self})
  return new
end,

_draw = function(self)
  --...
end,
}

Box = {
__index = BaseObject,

--!! important bit:
textColor = colors.white,
backgroundColor = colors.black,
--!!

new = function(self)
  local new = {}
  setmetatable(new, {__index = self})
  return new
end,

_draw = function(self)
  --...
end,
}

Then I have a third class (also inheriting from the base class) that contains an instance of the previous two classes:

TextField = {
__index = BaseObject,

box = nil,
label = nil,

textColor = colors.red,
backgroundColor = colors.white,

new = function(self, textc, backc)
  local new = {}
  setmetatable(new, {__index = self})
  new.box = Box:new(...)
  new.label = Label:new(...)

  -- Notice this bit
  new.textColor = textc
  new.backgroundColor = backc
  new.box.textColor = new.textColor
  new.box.backgroundColor = new.backgroundColor
  new.label.textColor = new.textColor
  new.label.backgroundColor = new.backgroundColor
end,
_draw = function(self)
  --...
end

textfield = TextField:new(colors.brown, colors.green)
-- blah stuffs stuffs

-- Notice this bit again
textfield.textColor = colors.orange
textfield.backgroundColor = colors.purple
textfield.box.textColor = ...
...
......
}

With the 2 "notice this bit"s in the last code example, how can I avoid having to set the box's and label's text and background colors every time I change the text field's text and background colors? How can I avoid having to do this once I create an instance of the TextField? Basically, how can I update the box's and the label's background and text colors as the textfield's one changes? I'd like to keep them all in sync, without having to do it manually with 6 lines of code each time. I would like for each of the classes to have their own text and background colors, as they can all be used independently.

Thanks for any help :)/>
Sorroko #2
Posted 25 August 2013 - 01:59 PM
Maybe you could pass a reference of the sub class to the super class, that way the super class can access the base classes variables. In this case pass a reference of TextField to Label & Box. And when drawing in the Box/Label class check if the sub class has a textColor variable, if not use the classes variable.

I'll get back to you in a bit and make my explanation clearer :P/>

EDIT: Another idea is to implement a kind of override system where sub classes override parent class variables/methods

EDIT2: Also all Label and Box variables should be defined in the 'new' object unless you want all Labels to have the same color

EDIT3: Finally, I think the most common solution is to make a setBackgroundColor and setTextColor function in TextField which sets the Label and Box fields. You can also call those functions from the TextField constructor. OOP mostly uses getters and setters
Kingdaro #3
Posted 25 August 2013 - 02:13 PM
Before I start, this entire system here is a little broken.

I notice in your Box and Label objects, you set the __index to base object, but that __index metamethod is never used, since the metatable is never set to those objects. I think this is what you want:
Spoiler

BaseObject = {
   tag = "";
   new = function(self, obj)
      setmetatable(obj, {__index = self})
      return obj
   end;
   -- ...
}

Label = BaseObject:new{
   textColor = colors.white;
   backgroundColor = colors.black;
   -- your draw/event functions or whatever
}

Box = BaseObject:new{
   textColor = colors.white;
   backgroundColor = colors.black;
   -- etc.
}

(Ignore my use of semicolons, personal preference :P/> )

In this code, the BaseObject's "new" function accepts a table to use as a base for the new object. So if I called BaseObject:new({hello = "world"}), then the instance would, along with having all of the properties of BaseObject, have the property "hello", which would equal "world". Because lua is nice, it allows us to drop the parentheses.

Using this concept, the Label and Box both become derivatives of the BaseObject. Even though in this logic, they'd be instances instead of superclasses like you would normally see them, it's still just a table just like the BaseObject, and all it's doing is inheriting all of BaseObject's variables and methods, including the new() function you've redefined for both of these classes.



Addressing the issue you're currently having, the way I'd do it is to overwrite the new function of the Label and Box objects, so that they accept two parameters, text color and background color, and set the parameters to the new object accordingly. Because Box is going to do the same, you can copy it pretty easily just by referencing the function from Label.

Spoiler

Label = BaseObject:new{
   textColor = colors.white;
   backgroundColor = colors.black;

   new = function(self, textc, backc)
      local obj = setmetatable({}, { __index = self })

      obj.textColor = textc or self.textColor
      obj.backgroundColor = backc or self.backgroundColor

      return obj
   end;

   -- your draw/event functions or whatever
}

Box = BaseObject:new{
   textColor = colors.white;
   backgroundColor = colors.black;
   new = Label.new;

   -- etc.
}

Then, under the TextField, you can simply pass them as arguments to the new Label and Box objects.

Spoiler

TextField = BaseObject:new{
   box = nil;
   label = nil;

   textColor = colors.red;
   backgroundColor = colors.white;

   new = function(self, textc, backc)
      local obj = {}

      setmetatable(obj, {__index = self})

      obj.textColor = textc
      obj.backgroundColor = backc
      obj.box = Box:new(textc, backc)
      obj.label = Label:new(textc, backc)

      return obj
   end;
}

To change the box and label's colors when the textfield's colors are changed, a method for it is probably the most straight-forward way of going about doing that.

Spoiler

TextField = BaseObject:new{
   -- ...
   setTextColor = function(self, color)
      self.textColor = color
      self.box.textColor = color
      self.label.textColor = color
   end;

   setBackgroundColor = function(self, color)
      self.backgroundColor = color
      self.box.backgroundColor = color
      self.label.backgroundColor = color
   end;
   -- ...
}
theoriginalbit #4
Posted 25 August 2013 - 02:48 PM
Ok so Kingdaro has a great example there! Much better writeup than I was working on for you. So what I've done is gone through and improved his code a little to remove variable that were not needed and also have it not need setters for the colours…

The thing to be mindful of, is with Lua, everything is pass-by-value, except tables, which are pass-by-reference… As such we can use this table pointer to our advantage and have them all reference this one colours table. Sadly it does mean I had to change the two variables "backgroundColor" and "textColor" into a single table "colors = {text=0, back=0}", but it works :)/>

The Code:
Spoiler

BaseObject = {
   new = function(self)
	  --# this is top level, it didn't need to allow for an index to anything other than itself
	  return setmetatable({}, {})
   end;
}

local function genericNew(super, textc, backc)
  local obj = BaseObject.new()
  --# if a text or back colour was supplied
  obj.colors = (textc or backc) and {
	--# set text to the supplied or black
	text = textc or 32768;
	--# set back to the supplied or 0
	back = backc or 0;
  --# if no colours were supplied, use the parents colour table
  } or super.colors

  --# set the index to super or the base object
  return setmetatable(obj, {__index = super})
end

Label = { new = genericNew }

Box = { new = genericNew }

TextField = {
   new = function(self, textc, backc)
	  local obj = BaseObject.new()
	  --# colours table
	  obj.colors = {
		text = textc or 32768;
		back = backc or 0;
	  }

	  setmetatable(obj, {__index = self})

	  --# create the box and label objects with a reference to this object
	  obj.box = Box.new(obj)
	  obj.label = Label.new(obj)

	  return obj
   end;
}

The Test Code:
Spoiler

local te = TextField:new(colors.blue, colors.orange)

print(te)
print(te.box)
print(te.label)

print(te.colors.back)
print(te.box.colors.back)
print(te.label.colors.back)

te.box.colors.back = colors.yellow

print(te.colors.back)
print(te.box.colors.back)
print(te.label.colors.back)
Edited on 25 August 2013 - 12:51 PM
Sorroko #5
Posted 25 August 2013 - 03:51 PM
Even though I did suggest passing a reference I personally think getters/setters are much better. But hey, they both work.
theoriginalbit #6
Posted 25 August 2013 - 04:21 PM
I personally think getters/setters are much better
In every programming class I've done at University I've been told the same thing "getters/setters are evil", I can't be bothered actually typing out all the reasons why, so here is a great article that the lecturers got us to read. :P/>

Article
Good alternative code example
jay5476 #7
Posted 25 August 2013 - 05:56 PM

    -- create a namespace
    Window = {}
    -- create the prototype with default values
    Window.prototype = {x=0, y=0, width=100, height=100, }
    -- create a metatable
    Window.mt = {}
    -- declare the constructor function
    function Window.new (o)
	  setmetatable(o, Window.mt)
	  return o
    end
   Window.mt.__index = function (table, key)
	  return Window.prototype[key]
    end
   w = Window.new{x=10, y=20}
   print(w.width)
   -- prints prototype width because there is no width in 'w'
   
i dont know if this is helpful to you
theoriginalbit #8
Posted 26 August 2013 - 02:37 AM
-snip-
Just slightly more verbose version of what Kingdaro posted.
GravityScore #9
Posted 26 August 2013 - 04:30 AM
Thanks for all the replies. It seems like the easiest route would just be a setter and getter (despite TOBIT's articles :P/>) on the TextField that changes everything for the label and box. It's very nice to know that tables are passed by reference in Lua - didn't know/realise that :)/>

@Kingdaro: I see what you mean (I think :P/>). The Box and Label aren't inheriting the Base's fields because the __index is set on the Box and Label table itself, not on its metatable (I think?). To be honest, the base class in full is a bit useless - it doesn't really contain anything important that I couldn't just add in elsewhere - so I think I'll get rid of it, but very useful help for future OOP inheritance problems :)/>

Second question I thought of now - what would you reckon would be the best method for multiple inheritance? Just compile the two superclass's tables into one and set that to the __index? Or is there a better way :P/>?
theoriginalbit #10
Posted 26 August 2013 - 04:47 AM
Thanks for all the replies. It seems like the easiest route would just be a setter and getter (despite BIT's articles :P/>)
Up to you, you are breaking the conventional OO paradigm though… :P/> Didn't see my solution that worked?


Second question I thought of now - what would you reckon would be the best method for multiple inheritance? Just compile the two superclass's tables into one and set that to the __index? Or is there a better way :P/>?

What circumstance would you need multiple inheritance? but yes, combining into one table would probably be best…

There is 2 major problems with multiple inheritance though… The first, which parent of it's super does it use… Assume the following
Class A — Base Class
Class B — Extends Class A
Class C — Extends Class A
Class D — Extends Class B
Class E — Extends Class C and D
Which becomes E's parent? If you combine C and D, does the super class become A or B?

The second problem is known as The Diamond Problem… I'll let you read that one…

These reasons are why we don't have multiple inheritance in most main stream languages (except C++). Although these languages give an alternative by implementing protocols/interfaces to provide SOME of the functionality of multiple inheritance.
Kingdaro #11
Posted 26 August 2013 - 06:13 AM
When I'm using classes (in like a love game or something) I don't really need to check the parent of the object. I just use a bunch of "obj.isBlah" booleans.

As for the "super" issue, that's only an issue if you make it one. Just don't use "super" at all. Use the method of the alternate class you need while providing your self.


SomeObject = {
  bleh = function(self) doSomething() end;
}

AnotherObject = {
  blehhhh = function(self) SomeObject.bleh(self) end;
}

That way, SomeObject.bleh will be called, while using the self of AnotherObject (or whatever instance of AnotherObject is.)

EDIT: Read up on the diamond issue, and it seems pretty simple to me. All of the inherited classes/interfaces/objects or whatever are inherited in order, and therefore in that example given by Wikipedia, if C were listed after B to be inherited, D would receive C's overwritten method of whatever A had. If you need B's method, just reference it:

D = {
  foo = B.foo;
}