Objective Lua Class

I have been working in creating a more object oriented approach to Lua programming for a while, and earlier posted an earlier revision of my object class, but it has grown a lot over the past month. The code is pretty thorough with comments, so most of the detailed explanations can be seen there.

object:new() - This is the primary constructor for new objects. This function can either create an empty object or one which references a table passed in the first argument. If metamethods are passed as keys in the source table, then they will be removed from the table and added to the metamethods of the tables metatable.

Object Environment Manipulation

Objects are also capable of temporarily assuming the global environment through the object:setScope() method or through a ‘for’ closure with the format for scope variable in object:inScopeOf() do. The inScopeOf method returns an iterator which sets the environment to the object for one loop and returnes it to its former state at the end of its loop. The object:pushScope() and object:popScope() methods can be used at any time to manipulate the environment stack. Calling object.setScope() with an object as the first argument will also push that object environment onto the top of the stack. Calling it with no arguments at any time will return the environment to _G. Exiting a for closure will however return the environment to its state before the for closure regardless of which object is referenced by the environment.

While in the environment of an object, all global variables indexed will become keys in the object and there is also a global variable self which holds the global environment. All keys of the object, including those inherited from parent object classes are also globals in this environment.

Object Extension Table References

Table extensions are something I just recently added and am still somewhat testing, but they allow you to basically create a selfness in he methods of another table. When you create an extension, a table is passed which becomes the source for the extension object. Methods called within this object pass the caller object as the first argument to these methods if they are called through the extension table returned from object:extension(table) .

I am going to be posting the code below because it was too large to fit into this one post, and I will also update this description to be better when I have more time later.


-- object.lua - (Beckett Dunning 2014) - Object oriented lua programming 
---------- ---------- ---------- ---------- ----------

local Lib_Version = 1.4

-- This script adds an Object Oriented aproach to Lua Programming Calls. Traditionally in lua, there is little notion of inheritance or classes. This script allows for Javascript like progamming calls in chaned sequence as opposed to the traditional structure of raw Lua

object,Libs = {},{} -- Stores Object Libraries
local meta = {__index = self,__type = "object class", __version = Lib_Version}
setmetatable(meta,{__type = "object meta", __index = object }) setmetatable(object,meta)

-- Local variable declaration for speed improvement
local type,pairs,table,unpack,setmetatable,getmetatable = 
 type,pairs,table,unpack,setmetatable,getmetatable
---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----

local meta_tables = { -- Stores Possible Meta-Table Key Values
 __index = false, __newindex = true, __call = true, __add = true, __sub = true, __mul = true, 
 __div = true, __unm = true, __concat = true, __len = true, __eq = true, __lt = true, __le = true}
local meta_ext = { __type = true, __version = true, __namespace = true}

------------------------------------------------------------------
-- Primative Object Constructor

-- The new metamethods for an object subclass are passed at the time of initialization. If metatable elements are detected, they are removed from the objects methods and added to its metatable. The imput object as well as the output object retured can be used to add new methods and values to a class, but new metamethods will not be detected after initial initialization.

object.new = function(self,entries) -- Generic object constructor 
 local object,meta = entries and type(entries) == "table" and entries or {}, 
  getmetatable(self):copy() meta.__index, meta. __type = self, "object"
 for key,value in pairs(object) do -- Updates object metatable by parsing passed table
  if meta_tables[key] or meta_ext[key] then meta[key],object[key] = value,nil end end
 setmetatable(object,meta) return object end -- Returns new object

------------------------------------------------------------------
-- Object Native Data Management Methods

-- An object is created with pointers to methods which can evaluate and modify data which is contained within an object. While these functions do exist in deeper object classes, they can be overriden by methods inherited from those classes.

object.keys = function(self) -- Returs array object of keys in an object
 local keys = object:new() for Key,_ in pairs(self) do keys[#keys + 1] = Key end
 return keys end -- Return: "array object"

object.length = function(self) return #self:keys() end -- Returns number of elements

object.insert = function(self,pos,value) -- Adds indexies / keys to objects
 if not self then error("Invalid argument no.1 'self' to object.insert().") return end 
 if pos and type(pos) == "number" and value then table.insert(self,pos,value) return value
 elseif pos and value then self[pos] = value return value
 elseif pos and not value then table.insert(self,pos) return pos
 else error("Invalid argument no.2 to object.insert().") return end end

object.remove = function(self,...) -- Removes indexies / keys from object entries
 if not self then error("Invalid argument no.1 'self' to object.remove().") end
 local arguments = {...} local length = #arguments 
        
 if length == 0 then return table.remove(self) 
 else local format for i = 1,length do local arg = arguments[i] format = type(arg)
  if format == "number" then arguments[i] = table.remove(self,arg)   
  elseif format == "string" then arguments[i] = self[arg] self[arg] = nil print("key removed")
  else arguments[i] = nil end end end 
 return unpack(arguments) end

object.indexOf = function(self,...) -- Parses table for indexes
 local search,entries = {...}, #self local num = #search
 for i = 1,num do local found for index = 1,entries do 
   if self[index] == search[i] then search[i] = index found = true break end end
  if not found then search[i] = nil end end 
 return unpack(search,1,num) end -- Returns list of first occurances  

object.keyOf = function(self,...) -- Parses table for key values
 local search,entries = {...}, #self local num = #search
 for i = 1,num do local found for key,value in pairs(self) do 
   if value == search[i] then search[i] = key found = true break end end
  if not found then search[i] = nil end end 
 return unpack(search,1,num) end -- Returns list of first occurances  

object.contains = function(self,...) -- Determines if object contains entries
 local arguments = {...} for i = 1, #arguments do local arg = arguments[i]
  if not self:indexOf(arg) and not self:keyOf(arg) then return false end end
 return true end -- Returns true or false

object.inverse = function(self) -- Inverses numerical indexies of array
 local pos = 0 for i = #self,1,-1 do i = i + pos
  self:insert(pos + 1,self:remove(i)) pos = pos + 1 end 
 return self end
    
------------------------------------------------------------------
-- Object Status / Configuration Methods

object.prototype = function(self) -- Returns prototype of object 
 if self == object then return self else return getmetatable(self).__index end end

object.type = function(self) -- Gets objects' __type values or type() results
 if not self then error("Invalid argument no.1 to object.type().",2) end
 local Meta = getmetatable(self) if Meta and Meta.__type then return Meta.__type 
 else return type(self) end end

object.copy = function(self) -- Creates a deep copy of object table and metatable
 local meta,metaFm,copy = {},getmetatable(self),{} for k,v in pairs(metaFm) do meta[k] = v end
 for key,value in pairs(self) do if type(value) ~= "table" then copy[key] = value
  else copy[key] = object.copy(value) end end setmetatable(copy,meta) 
 return copy end -- Returns: object - copy of object

 local meta_orig,meta = getmetatable(self) or {},{} for key,value in pairs(meta_orig) do 
  meta[key] = type(value) ~= "table" and value or object.copy(value) end
 
------------------------------------------------------------------
-- Extra Utility Methods

object.concat = function(self,sep)  -- Returns concantenated string of array entries
 return table.concat(self,sep) end -- Return: "string"



------------------------------------------------------------------
-- Object shortcut methods (WIP)

-- Extensions create objects which pass their caller object as the selfness of all method calls made through it. Through this process, methods can be called without explicitly needing to pass on a self reference and extending the range of the colon operator to be able to be placed in the keys of objects and still reference the original.

 local function subReference(obj,reference,entries,key) -- creates pointer method to object
  if not entries or not key or not entries[key] then return nil end
  local form = type(entries[key]) local call = form == "function" and entries[key] or 
   form == "table" and getmetatable(entries[key]).__call if not call then return entries[key] end
  local act = function(self,...) local target = self == reference and obj or self 
   return call(target,...) end -- calls nethods of indexed object
  if form == "function" then return act else local ref = {} setmetatable(ref,{__index = entries[key],
   __call = act}) return ref end end -- returns reformatted call methods

object.extension = function(self,entries) -- creates instance of shortcut in caller object
 if not self then error("Object reference for object.shortcut was invalid.") return end   
 local ext = {} setmetatable(ext,{__newindex = function() return end, -- New indexes are disguarded
  __index = function(reference,entry) return subReference(self,reference,entries,entry) end,
  __type = "object_ext", __call = subReference(self,ext,getmetatable(entries),"__call")}) 
 return ext end -- returns shortcut object

------------------------------------------------------------------
-- Envitonment Manipulation to scope of objects

-- The environment is set to a scope object which reference the caller object as well as passes all index and key declarations back to the caller object. Scopes also posess a 'self' which indexes the caller object. The current scope can be modified with the object:setScope(), object:pushScope(), and object:popScope() methods. Scopes can also exist in 'for' closures through the object:inScopeOf() iterator. Upon the loop ending in these closures, the enviornment prior to the loop is returned reguardless of the environment within the closure. 

---------------------------------------------------------------
-- Helper Functions ---------- ---------- ---------- ----------

 local function getLevel() -- Gets stack level of caller function
  local scope,err = 1,"no function environment for tail call at level "
  local env,error while true do env,error = pcall(getfenv,scope + 1) 
   if not env and error ~= err..tostring(scope + 1) then break end scope = scope + 1 end 
  return scope - 1 end -- Returns: number (stack level)      

 local function loadIndex(self,index) -- Helper function for scope index loading
  setfenv(1,_G) return Index end

----------------------------------------------------------------
-- Iterators (for (values) in (iterator) do) ---------- --------

object.inScopeOf = function(self) -- Sets environment within 'for' closure
 local cycle,env = 0, getfenv(getLevel() - 1) return function() cycle = cycle + 1 
  local scope = cycle == 1 and object.getScope(self) or cycle == 2 and env
  if scope then setfenv(getLevel() - 1, scope) -- Sets first to scope then _G
   return cycle == 1 and scope or nil end end end -- returns: scope object then nil

-------------------------------------------------------------------------------
-- Scope Stack Manipulation Methods ---------- ---------- ---------- ---------- 

object.getScope = function(self) -- Gets object's global scope   
 if not self then return getfenv(getLevel() - 1) end -- Returns caller environment
 local scope,meta = {self = self},{__type = "scope", __prev = getfenv(getLevel() - 1),
  __index = function(val,key) -- Indexes in environment point to object
   local value = self[key] or _G[key] if not value then return end 
   local format = type(value) local callable = format == "table" and getmetatable(value).__call
   return (format == "function" or (format == "table" and getmetatable(value).__call)) and 
    loadIndex(self,value) or value end, -- Returns index or calls scope function loader
  __newindex = function(t,k,v) self[k] = v end} -- New indexes point to original object
 setmetatable(scope,meta) return scope end -- Returns new scope of object

object.setScope = function(self) -- Sets global scope of caller function to object 
 if not self then return setfenv(getLevel() - 1, _G) end
 local scope,level = object.type(self) == "scope" and self or object.getScope(self), getLevel()
 local meta = getmetatable(getfenv(level - 1)) if meta then
  getmetatable(scope).__prev = getfenv(level - 1) end   
 return setfenv(getLevel() - 1, scope) end

object.pushScope = function(self) -- Pushes a copy of the current scope onto the stack
 local level = getLevel() local current = getfenv(level - 1)
 if object.type(current) == "scope" then local scope = current:copy() 
  getmetatable(scope).__prev = current -- Sets previous meta index to current environment
 return setfenv(level - 1, scope) end end -- Sets caller environment to scope copy

object.popScope = function(self) -- Removes current scope from top of stack
 local level = getLevel() local meta = getmetatable(getfenv(level - 1)) 
 if meta and meta.__prev then return setfenv(level - 1, meta.__prev) 
 else return error("No object is currently in scope.") end end

@Beckett2000 looks interesting.
But it is not obvious for me what functionnality you are trying to bring in. Could you add a short code example using the above in action?

@Jmv38 - I havnt posted any updates on this class in a while, but if you wanted to check out what it has become over the past few months, then check out this link: https://github.com/Beckett2000/Object.lua/blob/master/object.lua
My most recent version is 2.75, and has changed drastically from 1.4, though is now much too large to post here.

The object class uses metatables and meta methods to warp the way that you can write code in Lua under the hood, and provides ‘extensions’ which allow for extensive data management capabilities to objects. Objects are also all printable natively which allows ease of viewing with the __tostring metamethods. I will post a more extensive description when I have. Ore time, but the code is very well (perhaps excessively so) commented, so all usage can be seen from the comments therin. I have also developed many libraries which use this class, but am currently adapting them to work with the latest changes. If you would like to test out the code in action, you can copy and paste the code into a new tab, and then use the following code in the main setup loop:

`

 local baseClass = object()
 baseClass:ext():prefix().number = function(self) print(#self) end

print(baseClass.number)
baseClass.number.one = function(self) print(1,self[1]) end
print(baseClass.number)

baseClass:insertFirst("Bar")
baseClass.number:one()

local subClass = baseClass()
subClass:insertFirst("foo")

-- There are many ways to call an inherited extension...
subClass:numberone()
subClass.number:one()
subClass:number():one()

local value = object()
value.insert:First("Test")
value:insertLast(1,2,3,1,2,3)

for scope in value:inScopeOf() do

 print(self)
 self:removeFirstIndexOf("Test",1)
 print(self)

end

`

object:ext() To clarify a bit what features this class brings in, normally in Codea, subclasses reference their superclasses and are able to access methods therin through this process. One thing that I found however was that in classes with many ways of performing a function, sometimes there may be many functions which do a similar thing, but they all need to have custom name spaces, and if you assign a value to a standard table and then reference it, a ‘selfness’ will be passed of the table where the method is stored rather than referencing an object which stores this table of references.

Prefix extensions and Dictionary extensions in my object class both pass a ‘self’ reference in a function to the object which created them, so if you called self.insert:First(...), it would reference self rather than the insert extension table and insert a value into the first index of self.

Here is an example of the _insert.First method in use which is a type of prefix. All of the following valid lines of code would produce the same results:

object:insertFirst("test"); object.insert:First("test); object:insert():First("test") object.insert(object):First("test"); object:insert().First(object,"test")

The difference between prefix and dictionary extensions is that while they both perform this functionality, prefix extensions store their methods in the object or table which they belong to in a prefix|suffix| combination. IE: If you declared a new key to self.insert self.insert.Word = function(self) table.insert(self,"word") end it would actually be stored in self as self.insertWord. Extension dictionaries on the other hand store data in an internal datastore, so they do not modify the object which contains them, but they also cannot be called with with the one phrase syntax declaration above which prefixes are able to use.

Extensions can also be addeed to objects or tables using the object:ext():prefix/:dict methods. Methods can be declared in the call using an object:ext():prefix(name,method) format or a setter format object:ext():prefix().name = method. Once an extension is initialized, it works like a table stored within a table, and can be indexed and called from, but also with the added formatting options above.

Objects will also print out their keys and values when called using a tostring or print function and this is also recursive for objects within objects.