2

Context

I'm trying to write a class system which will generate melodies by performing permutations on the notes and rhythms. I'm gonna use this in the tool "xStream" in the "Renoise" software. My example here is a really dumbed down and generic version from what I really have.

I'm also making this as a learning opportunity. My OOP skills are weak, and I still haven't completely wrapped my head around metatables. So a preemptive sorry if I'm totally missing something. I'm doing all my code from the style of the basic OOP examples in the 3rd edition of Programming in Lua.

My question

What I want to do is have class methods with similar functionality all grouped into 'nested' tables. Eg, a table of 'melody permutations', a table of 'rhythm permutations', a table of miscellaneous utility methods, etc.

In the code, I have a class SomeClass. It has two types of print functions: print1_notes is in the 'main table' (ie, a key of SomeClass). print2.notes is in a 'nested' table print2, which is a key of SomeClass (ie, notes is just a key of SomeClass.print2).

I can call print1_notes just fine. The issue is when I call the print2 methods with the colon operator. If I don't use sugar (eg, obj.print2.notes(obj)), then no problem. But when I do (eg, obj.print2:notes()), I get errors about "attempt to (blah blah) a function value ...".

  1. How come it works without the colon operator?
  2. Do methods in SomeClass.print2 have access to the keys in SomeClass? Do they have access the same way that the non-nested table keys do?
  3. I was thinking that print2 needs an __index key to tell its methods to look up the keys in SomeClass when it doesn't know what self.a_key is. But self (inside the print2 methods) isn't a key. It's really just an alias for SomeClass. Is it though? Seems like there's a discrepancy with the nested table.
  4. Do I need to make SomeClass the metatable of print2? Is that even possible, since print2 isn't a separate table from SomeClass?
  5. Should I try a different approach? Maybe multiple inheritance?

Thanks. Sorry if this needs to be moved or if someone has asked this before.

My code

SomeClass = {
    new = function (self, t)
        t = t or {}
        setmetatable(t, self)
        self.__index = function (_, key)
            return self[key]
        end

        --should I add a setmetatable here? perhaps:
        --setmetatable(self.print2, self)

        return t
    end,


    notes = {},
    set_notes = function (self, t)
        self.notes = t or {}
        self.N = #self.notes
    end,

    print1_notes = function (self)
        print("There are "..tostring(self.N).." notes :", table.unpack(self.notes))
    end,


    --table of different print functions
    print2 = {
        notes = function (self)
            --is self an alias for SomeClass?
            assert(self.notes, "Error: self.notes = nil")
            print("There are "..tostring(self.N).." notes :", table.unpack(self.notes))             
        end,    

        first_note = function (self)
            fn = self.notes[1]
            print("first note is: ", fn)
        end,
    },

}



obj = SomeClass:new()
obj:set_notes{ 10,14,5, 10,14,5, 17 }


print("\ncalling print1_notes without sugar:")
obj.print1_notes(obj)
print("\ncalling print1_notes with sugar:")
obj:print1_notes()
print("\ncalling print2.notes without sugar")
obj.print2.notes(obj)
print("\ncalling print2.notes with sugar")
obj.print2:notes()  --this gives an error: "attempt to get length of a function value"


obj.print2.first_note(obj)  --this works fine
obj.print2:first_note()     --this gives an error: 
                            --  "attempt to index a function value (field 'notes')"

EDIT to code: instances of tostring(N) needed to be replaced with tostring(self.N).

EDIT: weird errors has to do with the fact that SomeClass.print2.notes has a notes member like SomeClass.notes. SomeClass.print2.first_note avoids this complication. (I'll explain more when I answer)

EDIT: I came up with a solution. It's not pretty, but it works. I'll post my answer below.

g8tr1522
  • 63
  • 6

3 Answers3

0

From your example, I guess that Class has a field notes as well, which you try to access in print2.notes()

The problem here is that lua doesn't really implement text book object orientation; if you call class:print_notes(...) you really just call class.print_notes(class, ...). If you want to call class.print2.notes(class) you can't do class.print2:notes(), because that is equivalent to calling class.print2.notes(class.print2). Neither can you write class:print2.notes() because that's invalid syntax; you can only use : to index functions and call them then and there.

EDIT: as for the error you get, class.print2.notes() probably tries accessing some notes member of class, which is probably a table, but because of the colon syntax, instead tries to access notes() in class.print2, which is a function and results in an error when trying to index it.

As for an actual solution, I'd say you should first of all reconsider the structure of your code. Organizing functions into namespaces inside classes is a somewhat weird approach, and a strong indicator that either your class is bloated and does more than it should, or that it shouldn't really be a class of its own but a library, several classes, or maybe even a simple function.

If either way the two print methods end up printing the notes, and notes is an array, why not extend that array with two print methods? The beauty of luas OO is that there's no clear line between objects and data, and as such it depends on the way you look at it. Try using that strength whenever possible, and don't stick too much to the text books with the OO design, that's not what lua is good at or was ever intended for.

DarkWiiPlayer
  • 6,871
  • 3
  • 23
  • 38
  • For more information on how the colon syntax works, I suggest reading this https://stackoverflow.com/questions/4911186/difference-between-and-in-lua/4911217#4911217 – DarkWiiPlayer Nov 09 '17 at 08:37
  • _"you can't do `class.print2:notes()`, because that is equivalent to calling `class.print2.notes(class.print2)`"._ This is exactly what I was missing. The manual didn't describe the colon syntax for nested tables, so this all makes sense now. – g8tr1522 Nov 10 '17 at 13:04
  • _"as for the error you get, class.print2.notes() probably tries accessing some notes member of class, which is probably a table, but because of the colon syntax, instead tries to access notes() in class.print2, which is a function and results in an error when trying to index it."_ Yep. Changed `print2.notes` to `print2.thenotes`. Any calls to `self.notes` was replaced with `nil` obviously. – g8tr1522 Nov 10 '17 at 13:05
  • _"As for an actual solution, I'd say you should first of all reconsider the structure of your code. ... your class is bloated and does more than it should, or that it shouldn't really be a class of its own but a library, several classes, or maybe even a simple function."_ Yes, this is definitely true. I said I was planning on writing this big class system, but I really should've said that I'm just in the early stages. I only have this one class right now, and I have plenty of room for new organization schemes in the future. Not to mention, how much I still need to learn in Lua, haha – g8tr1522 Nov 10 '17 at 13:11
  • Now realizing that `obj = SomeClass:new()` is actually `obj = SomeClass.new(SomeClass)`. This whole time I thought it was instead `obj = SomeClass.new(obj)`. \*facepalm\* This is what I get for copying and pasting code without understanding it. – g8tr1522 Nov 10 '17 at 19:03
  • So I still want to have the 'namespace' concept. Eg, `SomeClass.print` for any print related functions. I find `obj.print:notes()` more readable than `obj:print_notes()`. Or at the very least, easier on my brain when I'm recalling the members of `SomeClass`. **What do you recommend then, as an alternative?** Let me know if I should just open another post of course. – g8tr1522 Nov 10 '17 at 19:28
  • Ultimately, I want `SomeClass` to have `SomeClass.notes` and also `SomeClass.delays` which control the timings. I have a group of methods to manipulate the notes, and another distinct group that only manipulates the `delays` member. So it'd be much nicer to call `obj.m_notes:randomize()` or `obj.m_delays:randomize()`. Why would this be a bad idea? I figure I could just use multiple inheritance. `obj` would inherit the members of `m_notes` and `m_delays`. But again, I like the namespace there to remind me which member I'm using, even if they didn't have the same name `randomize()`. – g8tr1522 Nov 10 '17 at 19:36
0

After thinking about problem for a while, I noticed there is another approach to solving it and keeping the namespace idea.

Again, you can't use the colon syntax to pass the class to a function that's not in the class, but in a namespace of the class. What you can however do, is the following:

local function print2(instance)
  -- Does things
end

local function wrapper(namespace)
  print2(namespace.instance)
end

function someClass.new()
  ...
  notes = {print=wrapper,instance=t} -- every instance needs its own namespace table
  ...
end

as you can see, every instance has its own namespace table, but they all have a reference to the same shared function and a reference to the instance they belong to. When you call instance.notes:print() it ends up calling print on instance.notes, but the function only calls the real function on instance.notes.instance, which points back to instance

DarkWiiPlayer
  • 6,871
  • 3
  • 23
  • 38
0

I came up with a solution of sorts. In short: any functions in print2 must start with self = getmetatable(self). This is to turn self (originally obj.print2) into obj.

Also, sorry if this answer is too long or violates any guidelines.

To reiterate, I'm wanting to have another table SomeClass.print2 which has all print related functions. If I wanted to print the notes, I would do

obj.print2:notes()
--sugar for
obj.print2.notes(obj.print2)

Of course, when I do this, self.notes inside SomeClass.print2.notes would refer to obj.print2.notes (and not obj.notes). This is an issue even if I rename the function to SomeClass.print2.the_notes.

New Code

So what I did is changed SomeClass:new into this:

SomeClass:new = function (self, t)
    t = t or {}                     -- 
    setmetatable(t, self)           --
    self.__index = function (_, k)  --
        return self[k]              --
    end                             -- same from before

    mt = {}
    mt.print2 = {}
    mt.print2.__index = SomeClass.print2  -- only fixes calls to obj.print2:foo()

    t.print2 = {}
    setmetatable( t.print2, mt.print2)
    setmetatable(mt.print2, t)
    t.__index = t  -- fixes references to self.key inside print2 functions

    return t
end

And SomeClass.print2 should look like this now:

SomeClass.print2 = {
    notes = function (self)
        self = getmetatable(self)   -- self is now mt.print2
                                    -- self.notes will become obj.notes
        self = getmetatable(self)   -- self should just be obj now
                                    -- but this is unnecessary

        print("There are "..tostring(self.N).." notes :", table.unpack(self.notes))             
    end,    

    first_note = function (self)
        self = getmetatable(self)

        fn = self.notes[1]
        print("first note is: ", fn)
    end,
}

So basically, any calls to obj.print2.foo should return SomeClass.print2.foo. (See mt.print2.__index).

Every function in SomeClass.print2 MUST have at least one 'self = getmetatable(self)' at the top of the function body**. Then, inside SomeClass.print2.foo(), self is mt.print2. So then, self.key should become:

  • obj.key if key is notes or N
  • if obj.key is nil, then become SomeClass.key instead

** A second self = getmetatable(self) is recommended, but optional.

Very detailed explanation

Step-by-step, what we have is

  1. Create a new object with obj = SomeClass:new()
    • obj has an empty table at obj.print2
    • A new table mt is created, which is associated with obj
    • more specifically, mt.print2 is the metatable of obj.print2, and the metatable of mt.print2 is obj
  2. Set notes (eg, obj:set_notes{10,20,30})
  3. call obj.print2:notes(), ie, obj.print2.notes(obj.print2)
    • obj.print2.somekey is nil where 'somekey' is notes
    • So we look for the __index function in the metatable of obj.print2
    • mt.__index returns SomeClass.print2.somekey
    • So obj.print2.notes(obj.print2) is now SomeClass.print2.notes(obj.print2)
  4. Inside the call (part 1)
    • self is obj.print2
    • we want to get obj.notes using self.notes
    • So we change self into its metatable once. We do self = getmetatable(self).
    • self is now mt.print2
    • Now, if we did self.notes, this will be mt.print2.notes
      • mt.print2.notes is nil
      • This will call obj.__index because obj is the metatable for mt.print2
      • So mt.print2.notes should become obj.notes
      • Therefore, self.notes becomes obj.notes
  5. Inside the call (part 2)
    • self is mt.print2
    • We could do self = getmetatable(self) again
    • Then self would just become obj, since that's the metatable for mt.print2

Notes

This is a pretty ugly solution, I'll admit. But it works.

I hate having to put self = getmetatable(self) at the top of any function body in SomeClass.print2. This could be avoided if there was someway to transform self into obj inside of mt.print2.__index. I'm pretty sure that's impossible, since mt.print2.__index can only return an object (ie, the function SomeClass.print2.foo).

The reason I made mt a table with mt.print2 was so that I could add more of these 'namespaced' functions to SomeClass. If I wanted a namespace m_notes for functions that transform the notes, I would just add these statements to SomeClass:new():

mt.m_notes = {}
mt.m_notes.__index = SomeClass.m_notes

t.m_notes = {}
setmetatable( t.m_notes, mt.m_notes )
setmetatable(mt.m_notes, t)

In fact, I could make a function that does this for any new namespace:

add_namespace = function( t, mt, key_string )
    mt[key_string] = {}
    mt[key_string].__index = SomeClass[key_string]
     t[key_string] = {}
    setmetatable( t[key_string], mt[key_string] )
    setmetatable(mt[key_string], t)
end

Maybe it'd be better to make mt just be a member in t. So mt would just be t.mt and mt.print2 would be t.mt.print2, etc. Then it could be accessed later if needed (eg, for encapsulation purposes). There aren't any issues with having mt by itself; Calling SomeClass:new() will always create a new mt to be associated with the new object.

Please comment if you can think of any performace issues/enhancements with this trick. Or if I can make it more elegant looking. Definitely let me know if you can think of any way to remove the self = getmetatable(self) at the top of every print2 function definition.

g8tr1522
  • 63
  • 6