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

Bigtwisty's Base Class Api

Started by BigTwisty, 04 September 2013 - 09:38 PM
BigTwisty #1
Posted 04 September 2013 - 11:38 PM
Now presenting:

BigTwisty's Base Class API!!!
…for all your OOPy goodness…

This class structure can provide you with solid, protected interfaces for all your API classes.

pastebin link: http://pastebin.com/EMDVh4ub


Description:

- Provides a clean, size optimized interface to instances of custom classes.
- Provides basic debugging information to users of classes
- Provides configurable protection for your interface

Supported interface:
  • Instance table items
  • Because instances of your class are really just modified tables, they support basic table items by default. Unless the class has been locked or a class interface has already been defined by that name, creating table items works just like normal.
  • Members
  • Just like table items, but with type validation!
  • Methods
  • Just like table functions, but cannot be overwritten by the user.
  • Properties
  • Just like members but with extra behind-the-scenes functionality
  • Metamethods
  • All Lua supported metamethods except __index, __newindex and __metatable
Brief Tutorial:
SpoilerLets create a basic event class for the purpose of capturing event data to send to other objects. It should have a way to read the event id and parameters, and a way for the destination function to pass back whether the event has been handled.

Code:

function Event(...)
  local args = {...}
  -- automatically pull an event if no arguments are passed
  if #args == 0 then
	args = {os.pullEvent())
  end
  local self = {
	getId = function()
	  return args[1]
	end
	getParam = function(i)
	  return args[i+1]
	end
	getParamCount = function
	  return #args - 1
	end
	handled = false
	getString = function()
	  return table.concat(args,",")
	end
  }
  return self
end
Usage:

e = nil
repeat
  e = Event()
  print("Id:",e.getId()," ParamCount:",e.getParamCount())
  print(e.getString())
until e.getId()=="char" and e.getParam(1)=="q"

This is a typical class that protects its data in a closure. The problem is that all of these functions can be overwritten by the user of your API! Not only that, but what if they set event.handled to "Fo-Shizzle"! Hmmm… Enter BigTwisty's Base Class API!!!

Now let's look at the same class, written using the Base Class API:

Code:

os.loadAPI("btClass")--<link>https://www.dropbox.com/s/lo00wg78voq3uyx/btClass.lua</link>

-- ignores params set to ""
local function eventCompare(e1, e2)
  if e1.id ~= e2.id then return false end
  for i=1,math.min(e1.n, e2.n) do
	if e1[i] ~= "" and
	   e2[i] ~= "" and
	   e1[i] ~= e2[i] then
	   return false end
  end
  return true
end

function Event(...)
  local _args = {...}
  if #_args == 0 then _args = {os.pullEvent()} end
  local self = btClass.new()
  self._newProperty = { "id", get = _args[1] }
  self._newProperty = { "n", get = #_args - 1 }
  self._newMember = { "handled", false }
  self._newMethod = {
	"_getIndex",
	function(i) return _args[i+1] end,
	locked = true }
  self._newMetamethod = { "__tostring", function() return table.concat(_args, ",") end }
  self._newMetamethod = { "__eq", eventCompare }
  self._lock()
  return self
end

Usage:

e = nil
eQuit = Event("char", "q")

repeat
  e = Event()
  print("Id:",e.id," ParamCount:",e.n)
  print(e)
until e == eQuit
print(e[1])
e[55]=3

Notice how the user accesses the items as you would expect. Unlike with the previous method, your users can read any of the data, but can only change e.handled. If they try to assign anything but a boolean to e.hanlded, they will get useful debugging error data letting them know exactly how the pooch was screwed.

Note: The _getIndex method is a special case method that provides a function to return numerically indexed values directly from your class instance, if there has been no numeric key created for what is requested. If that method is locked, users cannot create new numeric keys for that class.

Note 2: If you want another class to inherit this one, you can still override the members, methods and properties, but the typical Joe would not be able to simply overwrite them with regular table items.

Detailed instructions:
Spoiler

Creating a new instance:

instance = class.new()

Members:
SpoilerFeatures:
  • Lock to the type of the initial value
  • Respond just like table items but are type validated when writing to
Declaration:

instance._newMember = {
  "memberName",
  <initial value>,
  [locked = boolean] }

Example:


instance = class.new()
instance._newMember = { "mem", 3 }
print(instance.mem)  --> 3
instance.mem = 10
print(instance.mem)  --> 10
instance.mem = "I'm gonna die!" --> Error: Expected number, got string

Methods:
SpoilerFeatures:
  • Respond like function members of the instance table
  • Cannot be overwritten accidentally
  • Can be obtionally configured to send a private table or variable (self) for instance size optimization
Declaration:


function(self, ...) -- if using self
function(...)	   -- if not using self

instance._newMethod = {
  "functionName",
  function,
  [self = table or value],
  [locked = bool] }

Example 1 (not using self):

instance = class.new()
instance._newMethod = { "multiply", multFunc }
	
instance.multiply(6) --> 12
instance.multiply = 2 --> Error: Attempted to write to a defined method

Example 2 (using self):

function multFunc(self, val)
  print((self.multiplier) * val)
end
instance = class.new()
instance._newMember = { "multiplier", 1 }
instance._newMethod = { "multiply", multFunc, self=instance }
instance.mult(6) --> 6
instance.multiplier = 3
instance.mult(6) --> 18

Properties:
SpoilerFeatures:
  • Responds like a variable member of the instance table
  • Provides additional behind-the-scenes functionality when reading from or writing to
  • Cannot be overwritten accidentally
  • Can be configured as read/write, read-only or write-only
  • Can be configured to always return a specific value when read from (no get function needed)
  • Can be configured to provide a "self" object to get and set functions
Declaration:

instance._newProperty = {
  "name",
  [get = function or value],
  [set = function],
  [self = table or value],
  [locked = boolean] }

Example:

local value = 3
function setVal(v)
  value = v
end
function getVal()
  return math.abs(value)
endfunction getReadOnly(self)
  return "Value of abs: "..tostring(self.abs)
end
instance = class.new()instance._newProperty = {
  "abs",
  get = getVal,
  set = setVal }
print(instance.abs)  --> 3
instance.abs = -55
print(instance.abs)  --> 55instance._newProperty = {
  "ro",
  get = getReadOnly,
  self = instance }
print(instance.ro)   --> Value of abs: 55
instance._newProperty = {
  "ro",
  get = "ReadOnlyResult" }print(instance.ro)   --> ReadOnlyResult
instance.ro = 3	  --> Error: Attempted to write to a read-only propertyinstance._newProperty = { "wo", set = setVal }
instance.wo = -20
print(instance.abs)  --> 20
print(instance.wo)   --> Error: Attempted to read from a write-only property

Metamethods:
SpoilerDescription:
  • Because the class object relies on metatable magic, the metatable is locked down.
  • Inserting metamethods is supported through the base class interface
Declaration:

instance._newMetamethod = {
  "metamethod name",
  function }

Example:

function mystring(self)
  return self.x
end
instance = class.new()
instance._newMember = { "x", "It worked!" }
instance._newMetamethod = { "__tostring", mystring }print(instance)  --> It worked!
instance._newMetamethod = { "__asdf", printString }  --> Error: Unsupported metamethod: __asdf


Lastly, please do not turn this thread into an argument on the principles of object oriented programming. If you think encapsulation and protection of your members isn't important, I will not argue the point with you. If you have constructive criticism about this implementation, however, I beg you to share it! I'm always excited to see new ways to do things…
BigTwisty #2
Posted 05 September 2013 - 07:54 PM
Updated: Discovered the handy error() function. This allows me to point users to where they attempted an illegal operation on the class, rather than the (useless to them) point in the class code where the attempt was determined to be illegal.

Also discovered the table.concat() function. Kind of a slap myself in the face and say "well duh" moment…
theoriginalbit #3
Posted 06 September 2013 - 02:01 AM
Very, very nice API :)/> I'm glad you made this into an API and released it :)/>

Updated: Discovered the handy error() function. This allows me to point users to where they attempted an illegal operation on the class, rather than the (useless to them) point in the class code where the attempt was determined to be illegal.
Also there is the throwback level of 0 for when you're wanting to error, and not print line numbers or the file the error has occurred on.
Also, as a tip there is the assert function, this is (as I'm sure you're aware) normally much cleaner to use, however in Lua it doesn't support throwback by default :(/> so ages ago I've made this override

local function assert( _cdn, _msg, _lvl )
  _lvl = tonumber(_lvl) or 1
  _lvl = _lvl == 0 and 0 or (_lvl + 1)
  if not _cdn then
	error(_msg or "assertion failed!", _lvl)
  end
  return _cdn
end

I feel that asserts can be a lot more readable, and also in most cases can be a form of readable compaction.

if type(value)	~= "table"	then error("Invalid metamethod: requires table", errLvls) end
if type(value[1]) ~= "string"   then error("Invalid metamethod name: string expected", errLvls) end
if type(value[2]) ~= "function" then error("Invalid metamethod function", errLvls) end
if metamethods[value[1]] == nil then error("Metamethod not supported: "..value[1], errLvls) end
Down to this

assert(type(value) == "table", "Invalid metamethod: requires table", errLvls)
assert(type(value[1]) == "string", "Invalid metamethod name: string expected", errLvls)
assert(type(value[2]) == "function", "Invalid metamethod function", errLvls)
assert(metamethods[value[1]], "Metamethod not supported: "..value[1], errLvls)
BigTwisty #4
Posted 06 September 2013 - 07:41 AM

local function assert( _cdn, _msg, _lvl )
  _lvl = tonumber(_lvl) or 1
  _lvl = _lvl == 0 and 0 or (_lvl + 1)
  if not _cdn then
	error(_msg or "assertion failed!", _lvl)
  end
  return _cdn
end

I like this a lot. Calls to this would be more readable.

And thanks for the positive feedback! I'd love to see what you do with this if you decide to play with it.
theoriginalbit #5
Posted 06 September 2013 - 08:16 AM
I like this a lot. Calls to this would be more readable.
Thanks :)/> that's why I made it, I was getting sick of the bad readability of my code when I was validating a large amount of arguments.

And thanks for the positive feedback! I'd love to see what you do with this if you decide to play with it.
I was starting to work on something, I put it back on the shelf though, 'cause I've been putting my uni work on the shelf too much lately, so need to work on that for a bit.
BigTwisty #6
Posted 08 September 2013 - 01:00 AM
Updates:

Ran extensive software validation, found many bugs. These should be now fixed.
theoriginalbit #7
Posted 08 September 2013 - 05:43 AM
Updates
I suggest that you return bool in your assert function (like I have with my override) for this use case

local handle = assert( fs.open("file", 'r'), "Cannot open file for read", 0)
Since in Lua a value evaluates to true and nil evaluates to false.
BigTwisty #8
Posted 08 September 2013 - 07:57 AM
I'll do that, but I'm curious. How would one close that file handle if you never capture it? Could that lead to a memory leak?
theoriginalbit #9
Posted 08 September 2013 - 08:10 AM
I'll do that, but I'm curious. How would one close that file handle if you never capture it? Could that lead to a memory leak?
Thats the point I was making, with yours, because you never return the variable bool, the variable handle never contains the table to interact with the file.
However with the default assert, and the assert override I made, the variable that is passed in to check is also returned, this means that the handle gets returned into the handle variable. But obviously when the passed in variable is nil, the assert function will error.

EDIT: Oh and not really to the memory leak. There would be the table in memory, which gets cleaned up when no longer being referenced, so it would get cleaned up almost immediately. The problem with not closing a file is the fact that there is still a file handle open in the real OS.
Edited on 08 September 2013 - 06:13 AM
BigTwisty #10
Posted 08 September 2013 - 08:21 AM
The problem with not closing a file is the fact that there is still a file handle open in the real OS.
That was what I was talking about. An open file handle in the OS takes up memory. Not much, but if that code were to be some loop somewhere, the handles could add up.

I wasn't questioning the wisdom of passing the bool back. Anything that adds that much usefulness to a function with no overhead is good.
theoriginalbit #11
Posted 08 September 2013 - 01:20 PM
That was what I was talking about. An open file handle in the OS takes up memory. Not much, but if that code were to be some loop somewhere, the handles could add up.
Well if you prefer I will update my use case :P/> ……

I suggest that you return bool in your assert function for this use case

local handle = assert(fs.open("someFile", 'r'), "Cannot open file for read", 0)
print(handle.readAll()) --# a purpose for opening the file
handle.close() --# and now the handle is gone too
Although it doesn't add any useful content or meaning to the point I was trying to make, the handle is now closed when it has been successfully opened :P/>
BigTwisty #12
Posted 08 September 2013 - 02:01 PM
Although it doesn't add any useful content or meaning to the point I was trying to make, the handle is now closed when it has been successfully opened :P/>

I understood what you were trying to get at, as I mentioned earlier. Free floating file handles just scream bloody murder, and my coder's OCD would let me let it slide… :wacko:/>

This would actually be far more useful if Lua handled value assignments better. Many other languages pass the assigned value on back, allowing for lines like this:

x = y = 3
This would set both x and y to 3. It would also allow better use of assert, like this:

assert(h = fs.open("yourMotherWasAHampster.txt"), "Invalid French accent")

Edit: I'm an idiot. I didn't actually read your code! (working on an iPhone) That would work quite well! I still stand by my x = y = 3, however…

I updated btClass.assert() to account for non boolean test values. Because I already had a validation script for btClass, I was able to verify in seconds that nothing else broke! Have I mentioned that I like my new validation API?
theoriginalbit #13
Posted 08 September 2013 - 02:06 PM
This would actually be far more useful if Lua handled value assignments better. Many other languages pass the assigned value on back, allowing for lines like this:

x = y = 3
This would set both x and y to 3. It would also allow better use of assert, like this:

assert(h = fs.open("yourMotherWasAHampster.txt"), "Invalid French accent")
Yeh but

h = assert(fs.open("file",'r'), "failed")
is better than nothing. Also if it did handle value assignments better, would allow for things like this

lines = {}
h = fs.open('file','r')
while s = h ~= nil do
  table.insert(lines, s)
end
BigTwisty #14
Posted 08 September 2013 - 02:27 PM
Also if it did handle value assignments better, would allow for things like this

lines = {}
h = fs.open('file','r')
while s = h ~= nil do
  table.insert(lines, s)
end

I think that would be…

lines = {}
h = fs.open('file','r')
while (s = h.readLine()) ~= nil do
  table.insert(lines, s)
end
… to make sure it wasn't comparing h.readLine() to nil, possibly making s = true. ;)/>

edit: Actually…

while s = h.readLine() do
… would work just fine!
theoriginalbit #15
Posted 19 September 2013 - 05:45 PM
Have you considered an extends or implements function?
Symmetryc #16
Posted 19 September 2013 - 09:11 PM
Also if it did handle value assignments better, would allow for things like this

lines = {}
h = fs.open('file','r')
while s = h ~= nil do
  table.insert(lines, s)
end

I think that would be…

lines = {}
h = fs.open('file','r')
while (s = h.readLine()) ~= nil do
  table.insert(lines, s)
end
… to make sure it wasn't comparing h.readLine() to nil, possibly making s = true. ;)/>

edit: Actually…

while s = h.readLine() do
… would work just fine!
I have not a clue what the OP is about, but I just thought I'd pop in and say that you could also did this (without this API or anything, in standard CC):

local allLines = {}
local h = fs.open("something", "r")
for line in h.readLine() do
  table.insert(allLines, line)
end
theoriginalbit #17
Posted 19 September 2013 - 09:14 PM
I have not a clue what the OP is about, but I just thought I'd pop in and say that you could also did this (without this API or anything, in standard CC):

local allLines = {}
local h = fs.open("something", "r")
for line in h.readLine() do
  table.insert(allLines, line)
end
We went off on our own little tangent, but please make sure you at least know the tangent before posting. We were discussing the way Lua handles variable assignments and about what we could do if it handled it better.
Symmetryc #18
Posted 19 September 2013 - 09:49 PM
I have not a clue what the OP is about, but I just thought I'd pop in and say that you could also did this (without this API or anything, in standard CC):
 local allLines = {} local h = fs.open("something", "r") for line in h.readLine() do table.insert(allLines, line) end 
We went off on our own little tangent, but please make sure you at least know the tangent before posting. We were discussing the way Lua handles variable assignments and about what we could do if it handled it better.
Sorry :P/>. Even though I didn't fully know what you were talking about, my post is still pretty relevant atleast :)/>.
theoriginalbit #19
Posted 19 September 2013 - 09:51 PM
Sorry :P/>. Even though I didn't fully know what you were talking about, my post is still pretty relevant atleast :)/>.
Its ok :)/> its relevant in terms of its what we can do currently, but in terms of what we can do with better variable assignment its not at all relevant.