Creating a class - beginner question

Is there any benefit in using the first example instead of the second when constructing a class? I was just guessing that the first way is some Renoise specific sugar that sets up the metatable automatically for you. Am I right?

Example 1:

class "Object"

function Object:__init()
 self.value = "hello"
end

Example 2:

Object = { }

function Object:new()
 self.value = "hello"
 setmetatable(self, Object)
end

Small additional question: Would it be considered good and common practice to set up the __tostring meta method to just return the object (incl metatable)? Is this a neccessary and proper way to allow code like: new_object = old_object ? (I am guessing that I’m horribly wrong here)

guessing that the first way is some Renoise specific sugar that sets up the metatable automatically for you.

The Renoise API is using something called LuaBindto achieve OO functionality. Basically, it allows you to specify inheritance and getter/setter properties, something a standard lua table does not have.
And yet, inheritance is not always needed - you can use a coding style completely without using any of these principles.

One major advantage of classes is that all the code is initialized at the same time.
In practice, this means that the order in which functions are specified no longer carries any importance - you can call a function “from the top” which reference something near the “bottom”. This really made me embrace classes more than anything else.

new_object = old_object

I guess you are trying to copy over the state of one object into another?

Usually, the ability to “clone” objects (including their properties) requires something called reflection, which the Renoise API doesn’t really have.
But, depending on what you want to achieve here, maybethis codecould be of use?

If you want to save the state into a document (tool preferences, etc.), it might be worth extending the renoise.Document.DocumentNode to begin with (but be warned, the document class can be a bit painful to work with).

In practice, this means that the order in which functions are specified no longer carries any importance - you can call a function “from the top” which reference something near the “bottom”. This really made me embrace classes more than anything else.

That sounds great enough!

new_object = old_object

I guess you are trying to copy over the state of one object into another?

Usually, the ability to “clone” objects (including their properties) requires something called reflection, which the Renoise API doesn’t really have.
But, depending on what you want to achieve here, maybethis codecould be of use?

If you want to save the state into a document (tool preferences, etc.), it might be worth extending the renoise.Document.DocumentNode to begin with (but be warned, the document class can be a bit painful to work with).

Isn’t it easier to return the object from a function? You could then do something like

function My_class:return()
  local o = {}
  for k, val in pairs(self) do
    o[k] = val -- or something streamlined if needed
  end
  setmetatable(My_class, o)
  return o
end

new_object = old_object:return()

Just wondering what the most common way of doing it is (syntax wise). Perhaps combining this with a constructor, and then do old_object:__init(new_object)?

Isn’t it easier to return the object from a function?

A bit like reinventing the wheel: sometimes it makes sense because it’s good practice :slight_smile:

There are approximately 1 million different ways of doing this… I often use a table based constructor argument, like this:

SomeClass{
 some = "foo",
 property = "bar",
}

I like it for it’s readability. You need to “unpack” the arguments in the constructor though.

Also, I want to point out that in a class, you can use_some_but not _all_meta-methods (__tostring, __eq, __le,__lt.).

However, you can’t assign a metatable as such. In a way, the class_is_the metatable.

Thanks danoise! Just want to make sure I’m not breaking any common sense rules and upsetting any puritans (i e my future self).

Not sure I agree 100% about your statement about metatables. At least not technically (but yes, practically). I believe that you could set a unique metatable for a specific object, while maintaining the class functions (which are basically just references to function() table entries via __index). More specifically, you could technically have __index and __eq pointing to two different classes/tables by dynamically generating a tailored metatable (?)

More specifically, you could technically have __index and __eq pointing to two different classes/tables

You can’t use __index in luabind AFAIK. You can add a table as a property of the class and do your voodoo there, but a metatable can’t be passed to the class itself.

Again, depending on what you want to do (?), with luabind something like __index is replaced by a much more friendly getter/setter interface.

class 'MyClass'

function MyClass:__init()
  self.magic = property(self.get_magic,self.set_magic)
  self._magic = "abracadabra"
end

function MyClass:get_magic()
  return self._magic
end

function MyClass:set_magic(val)
  assert(type(val)=="string","Magic is made from words")
  self._magic = val
end

This approach can be used in a number of ways. Most importantly, to validate input type (as in the example above) and respond to input in some way.

Of course, you could do all of the above with a function called “set_magic” and by requesting the self._magic property when needed - but having a single property, “magic”, makes the code more readable IMO.

Replace the “_magic” with an observable value and you have additional benefits - as you can only assign correct types to observable values, you can skip the assert. And, well, it becomes observable.

This eludes me. What does “self.magic = property(self.get_magic,self.set_magic)” achieve here?

Like the __index metamethod on tables, properties on a class can control access.

In the example above, MyClass will refuse attempts to set the magic property to a non-string value, and returns a detailed error message when doing so.

-- read-only
self.magic = property(self.get_magic)

-- read and write
self.magic = property(self.get_magic,self.set_magic)

The function names are not special. I prefer to prefix the property name with get_ and set_, but it’s really the order of arguments for the property statement which are important.

Once you have defined a property, calling it by name will invoke the getter or setter method instead of just setting a value.

OK, so it’s a way to define setter/getter for a key. (I can see some other uses for this as well, other than verifying the input. If you need to use a hash table, or some kind of cloned table, the setter could handle this which is kind of neat).

The drawback seems to be that you need a separate entry that handles getting/setting, but the actual data is stored in _magic?

I would imagine that a common thing to do is to define your properties in a table, and iterate that in a for loop to generate generic getter/setter functions for all keys? Is that possible?

class_values = { foo, bar, hey }
for k, val in ipairs(class_values) do
 my_class:class_values[k]_getter() -- wrong syntax here? how to concatenate?
 return self._class_values[k] -- here too?
 end
end

PS. I’m a bit worried about performance issues with this model compared to standard lua “classes” (table __index based). Maybe I don’t need to be?

The drawback seems to be that you need a separate entry that handles getting/setting, but the actual data is stored in _magic?

Well, you would have to store it somewhere. Prefixing a variable with underscore is the usual OO way of denoting a “private” variable.

And no, I’m pretty sure there is no noticeable impact on performance. After all, you_want_ to usethose getter/setter methods, or you wouldn’t go for this approach :slight_smile:

But even then, not being a “true” private/protected property you could still access the “raw” _magic property from outside of the class and this would yield zero processing overhead. Best of both worlds?

When it comes iterating through class properties, you can also access them with bracketized syntax. So our magic property could be accessed as my_class[_magic].

Gives you the ability to expose property names in some standardized manner (this seems to be what you want? ).

Well, it’s just a detail, but I was thinking

  1. Just to confirm: you generally don’t want to use underscore when referencing anything in your code. class._magic is just where the data “secretly” resides.

  2. For most keys that you set as properties, you are probably using the same generic getter function.

  3. There is some way to quickly set up a generic getter function for all properties (?)

Or will I have to manually define separate getter functions for each and every property within the class? That’s a bit ugly imo.

PS. Another nifty usage for a setter would be the ability to accept “C-4” as well as a note value when storing a note value :slight_smile:

Honestly, I would not be so keen to come up with a “generic” solution to begin with.

Just experiment a bit and see what kind of approach works best.

When it comes to note value and string representations, check out this class:

https://github.com/renoise/xrnx/blob/master/Tools/com.renoise.xStream.xrnx/source/xLib/classes/xNoteColumn.lua

It’s making heavy use of the principles we’ve discussed here :wink:

I am having some trouble “converting” my class to a proper luabind class. Specifically, how do I prevent certain keys from the class to be copied into the object?

I am assuming that luabind classes are using 100% of the class as template per default, with no need of the constructor copying the class table with a for loop.

I’m also assuming (wrongly?) that I don’t have to use getters/setters if I don’t really want to.

This is where I am at right now. As seen, line 76- is now obsolete (was only relevant in my old table based class), but tells what I want to achieve. Any idea?

PS: will change to __init() later on

Click to view contents
local xinc -- for testing only
class 'osc'
osc.DEFAULT_TYPE = "min"
osc.DEFAULT_MIN_VALUE = 0
osc.DEFAULT_MAX_VALUE = 255
osc.DEFAULT_PHASE = 0
osc.DEFAULT_RESOLUTION = 1
osc.func = {
    { user_abr = { "sine", "sin" },
      calc = function(phase) return (math.sin(math.rad(360 * phase)) / 2 + 0.5) end },
    { user_abr = { "square", "pulse", "sqr", "pul" },
      calc = function(phase) if (phase <= 0.5) then return 1 else return 0 end end },
    { user_abr = { "random", "rnd", "noise" },
      calc = function(phase) return math.random() end },
    { user_abr = { "tri", "triangle" },
      calc = function(phase) return math.abs(((phase+0.75) % 1) - 0.5) * 2 end },
    { user_abr = { "saw", "ramp", "sawtooth" },
      calc = function(phase) return phase end },
    { user_abr = { "min" },
      calc = function() return 0 end },
    { user_abr = { "max" },
      calc = function() return 1 end },
  }
osc.get_index = function(user_type)
  for i_osc, osc in ipairs(osc.func) do
    for i_abr, abr in ipairs(osc.user_abr) do
      if (abr == user_type) then return i_osc end
    end
  end
  return osc.DEFAULT_TYPE
end
function osc:runtime()
  return xinc - self.start_xinc
end
function osc:reset(phase)
  self.start_xinc, self.phase = xinc, phase or self.phase
end
function osc:value(scale_min, scale_max, resolution)
print(self.phase)
    local phase = self:runtime() % self.freq / self.freq + self.phase
    local min, max, res = scale_min or self.min_value, scale_max or self.max_value, resolution or self.resolution
    local value = osc.func[self.type].calc(phase) * (max-min) + min
    return math.max(min, math.min(max, math.floor(value/res+0.500001) * res))
end
function osc:__tostring()
  return self:value()
end
function osc.create(type, freq, min_value, max_value, phase, resolution)
  local object
  local args_valid = tonumber(freq) and type and
                     (min_value == nil or tonumber(min_value)) and
                     (max_value == nil or tonumber(max_value)) and
                     (phase == nil or (tonumber(phase) and phase <= 1 and phase >= 0)) and
                     (resolution == nil or tonumber(resolution))
  if args_valid then
    object = {
      type = osc.get_index(type),
      freq = freq,
      min_value = min_value or osc.DEFAULT_MIN_VALUE,
      max_value = max_value or osc.DEFAULT_MAX_VALUE,
      resolution = resolution or osc.DEFAULT_RESOLUTION,
      phase = phase or osc.DEFAULT_PHASE,
      start_xinc = xinc
    }
    for k, val in pairs(osc) do
      if not (k == "func") or (k == "get_index") then
        object[k] = val
      end
    end
  end
  return object
end
-- testing below
for i = 1, 32 do xinc = i
  if xinc == 1 then my_osc = osc.create("sine", 16, 0, 1) end
  print(my_osc)
end

I am having some trouble “converting” my class to a proper luabind class. Specifically, how do I prevent certain keys from the class to be copied into the object?

It wasn’t really obvious to me from the code what ‘keys’ you are referring to?

If it’s about coming up with an efficient way to reach the relevant ‘calc’ method for the type (sine, saw, etc.), then I would suggest that you organize the lookup slightly differently.
Basically, use associativetables instead of indexedones. Then you could directlylook up the method using the type as argument:

osc.foo = function (arg) return bla end,
osc.bar = function (arg) return bla end,
osc.func = {
  ["sine"] = osc.foo,
  ["sin"] = osc.foo,
  ["square"] = osc.bar,
  ["pulse"] = osc.bar,
  ["sqr"] = osc.bar,
  ["pul"] = osc.bar,
  -- etc...
}

The lack of __init is a bit confusing to me, because it makes the implementation look more like a ‘factory’ pattern - https://en.wikipedia.org/wiki/Factory_method_pattern.
I think would be much more obvious to use __init right away?

Thanks Danoise! Things are slowly becoming more clear to me. (I’ve found that an oprint(self) gives me a good sense of how the object should be structured, similar to other renoise objects - what to show, what to hide and how to set.)

Now I just have a small problem with the __add metamethod. It seems that __tostring isn’t applied (relevant) inside the _add method.

function osc:__tostring()
return self:current_value()
end

function osc:__add(arg)
return self:current_value() + arg
-- arg probably needs to be handled some special way for this to work, but I don't know how. __tostring doesn't seem to be applied here.
end

print(osc_a + osc_c) -- gives an error

It really would make sense if this worked, I think. Adding a normal number works, but not adding two objects.

EDIT: Solved. I don’t quite like it but this seems to work for _add. It doesn’t work for __mod, however. Perhaps a bug in LUA?

function osc:__add(arg)
 if type(arg) == "osc" then
 return self:current_value() + arg:current_value()
 else
 return self:current_value() + arg
 end
end

EDIT: Solved. I don’t quite like it but this seems to work for _add. It doesn’t work for __mod, however. Perhaps a bug in LUA?

I did not completely research every metamethod, but I know that the luabind implementation does not cover them all.

For example, I believe the “index” and “newindex” are off-limits. Luabind instead introduces the “property” (get/set interface).

Also, it’s sometimes harder to tell what is going on with metamethods - for example, I once wondered why my __tostring didn’t work as expected until I realized that error messages are not reported from there.

That’s ok!

EDIT: Found the answer to a question I posted about exposed methods here: https://forum.renoise.com/t/classes-and-adding-properties/42855

A “cleaner” syntax seems to be the following - at least something to be aware of. This will keep objects tidy if you don’t want to expose all the setter/getter methods.

MyClass.my_property = property(
 function(obj) return obj._my_property end,
 function(obj, value) obj._my_property = value end
 )

I am not sure what the Renoise tradition is, but I believe that exposing a set method is only relevant when something needs to be set by using several arguments (or one argument being interpreted to the value, but is not the value)

PS. Regarding the __mod metamethod, it doesn’t seem to be available in luabind. It is available in lua, though. Maybe it’s possible to ‘activate’ it by mixing in some setmetatable in the luabind class, but I’m not sure.