monome / norns

norns is many sound instruments.
http://monome.org
GNU General Public License v3.0
629 stars 145 forks source link

lua API and OOP #38

Closed catfact closed 6 years ago

catfact commented 6 years ago

still getting to grips with lua style, so ATM it is all over the place.

i'd like to adopt a consistent and intuitive OOP style that wraps all C functions in class methods. for example, the Grid API is pretty clean; user gets a Grid object and uses methods on it, never having to know about the global grid_refresh, et al: https://github.com/catfact/norns/blob/dev/lua/sys/monome.lua#L18

i'd like to extend this approach to Engine, Poll, Timer, Input, and whatever else we end up adding.

i think it's ok for norns to remain a singleton, manager-pattern class (global table) which is responsible for serving up these API objects based on the actual system resources.

catfact commented 6 years ago

it took a minute to figure out, so here's a clean implementation of custom getter/setter/constructor methods for a class called Foo with a property called bar:

-- test some object oriented methods using metatables

-- metatable for class
local Foo = {}
Foo.__index = Foo

-- constructor without .new
setmetatable(Foo, {
   __call = function (cls, ...)
      return cls.new(...)
   end
})

-- the "real" constructor
function Foo.new(val)
   local self = setmetatable({}, Foo)
   -- keep object state (aka properties, members) in this "private" table
   self.state = { bar = val } 
   return self
end

-- custom getter
function Foo:__index(idx)
   if idx == "bar" then
      print( "getting " .. idx )
      return self.state[idx]
   else
      return rawget( self, idx )
   end
end

-- custom setter
function Foo:__newindex(idx, val)
   if idx == "bar" then
      self.state[idx] = val
      print( "setting "..idx.." : "..val )
   else
      rawset( self, idx, val )
   end
end

return Foo

custom setter is important for this approach; for example we would want to do stuff like:

 -- get a Poll object
p = Poll('amp_in_l')
if p == nil then print("warning: unknown poll label; this Poll won't do anything") 
-- set a callback, which is a normal member property
p.callback = function(val) print(val) end
-- set the callback period; this is a custom setter that calls poll_set_time under the hood
p.period = 0.02
-- instance method; calls the poll_start() C function under the hood
p:start()
tehn commented 6 years ago

the example code at the end is perfect for end-user script manipulation. this sort of abstraction is exactly what i was looking for.

On Wed, Oct 25, 2017 at 1:10 AM, ezra buchla notifications@github.com wrote:

it took a minute to figure out, so here's a clean implementation of custom getter/setter/constructor methods for a class called Foo with a property called bar:

-- test some object oriented methods using metatables local Foo = {} Foo.index = Foo -- constructor without .newsetmetatable(Foo, { call = function (cls, ...) return cls.new(...) end }) -- custom getterfunction Foo:index(idx) if idx == "bar" then print( "getting " .. idx ) return self.state[idx] else return rawget( self, idx ) endend -- custom setterfunction Foo:newindex(idx, val) if idx == "bar" then self.state[idx] = val print( "set member "..idx.." to "..val ) else rawset( self, idx, val ) endend -- the real constructorfunction Foo.new(val) local self = setmetatable({}, Foo) self.state = { bar = val } return self endidL return Foo

custom setter is important for this approach; for example we would want to do stuff like:

-- get a Poll object p = Poll('amp_in_l') -- set a callback, which is a normal member property p.callback = function(val) print(val) end-- set the callback period; this is a custom setter that calls poll_set_time under the hood p.period = 0.02-- instance method; calls the poll_start() C function under the hood p:start()

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/catfact/norns/issues/38#issuecomment-339216341, or mute the thread https://github.com/notifications/unsubscribe-auth/AAPEcNlUMolARPoWqcETMGt2547XTfYMks5svsLbgaJpZM4QFcMb .

catfact commented 6 years ago

cool - yeah, i'm rewriting all the lua now with a clearer understanding both of the language and what we need from it.

doing what i should have done in the first place, which is write the script first against imaginary API, and then work backwards.

here's one idea i'm working with right now, lmk what you think. it should do the following things:

local norns = require 'norns'
local engine = require 'engine'
local poll = require 'poll'
local input = require 'input'

local state = {
   use_pitch = false,
   pitch_poll = nil
   gamepad = nil
}

local butCode = 'BTN_SOUTH'

norns.deinit = function()
   state.pitch_poll.stop
   state = nil
   gamepad.unsetCallback(butCode)
end

local didLoadEngine = function(commands)
   -- get the 
   local p = poll.findByLabel('pitch_l')
   p.time = (0.25)
   p.callback = function( hz )
      if state.on then e.hz(hz) end
   end
   state.pitch_poll = p
   -- request input device list with callback
   input.getDevices( didGetDevices )
end

local buttonCallback = function(butState)
   state.use_pitch = butState
end

local didGetDevices = function(devices)
   -- FIXME: loop over devices here until we find one with btn_south
   -- here just assume there's only one device   
   gamepad = devices[1]
   if gamepad.hasCode(butCode) then
      gamepad.setCallback(butCode, buttonCallback)
   else
      print("warning: connected device doesn't appear to be a gamepad")
   end
end

-- load the desired engine with our callback
engine.load('test-sine', didLoadEngine)
catfact commented 6 years ago

cool, that's good to hear

currently i'm rewriting much of the lua with a clearer understanding of both the language and our needs.

this is what i probably should have done in the first place: write the script first against nonexistent API, then work backwards.

currently i have something like this for a stupid demo. it should do the following:

of particular note is the use of functions as first-class objects. anytime a function call should logically execute an action asynchronously on completion (e.g. engine load, input device query), that function should just accept a callback parameter.

local norns = require 'norns'
local engine = require 'engine'
local poll = require 'poll'
local input = require 'input'

local state = {
   use_pitch = false,
   pitch_poll = nil
   gamepad = nil
}

local butCode = 'BTN_SOUTH'

local buttonCallback = function(butState)
   state.use_pitch = butState
end

local hzCallback = function(hz)
   if state.use_pitch then engine.hz(hz) end
end

local didLoadEngine = function(commands)
   local p = poll.findByLabel('pitch_l')
   if p then
      -- start the poll with callback and period
      p:start(hzCallback, 0.25) 
      -- store the poll in our global state so we can stop it later
      state.pitch_poll = p
   else
      print("warning: couldn't find requested poll label")
   end
   -- request input device list with callback
   input.getDevices( didGetDevices )
end

local didGetDevices = function(devices)
   -- FIXME: loop over devices here until we find one with btn_south
   -- here just assume there's only one device   
   gamepad = devices[1]
   if gamepad.hasCode(butCode) then
      gamepad.setCallback(butCode, buttonCallback)
   else
      print("warning: connected device doesn't appear to be a gamepad")
   end
end

-- load the desired engine with our callback
engine.load('test-sine', didLoadEngine)

-- register cleanup function
norns.deinit = function()
   state.pitch_poll.stop
   state.gamepad.unsetCallback(butCode)
   state = nil
   butCode = nil
end
tehn commented 6 years ago

i'm going to close this as the current modules seem fine and we can revisit in the future if needed.