The main difference between the two methods is that with metatables, classes are defined as tables. In this method, they're defined by functions. Let's take a look at a very simple way of accomplishing this.
function MyClass(t)
local object = {}
local parameter = t
function object.testMethod()
print(parameter)
end
return object
end
obj = MyClass("Parameter")
obj.test()
The MyClass function lays out what methods will be put into an object table, and returns the object. This is very basic, but it's a good way to demonstrate the capabilities of functional OOP. But what are those capabilities?
- Private instance variables In metatable OOP, all of an objects instance variables must be in the object table. This makes them accessible to absolutely anything. In functional OOP, we can declare a local variable each time MyClass is called so that an object's methods can access that variable, but external code cannot.
- You cannot modify the class, only the objects Metatable OOP has a security vulnerability. If an API creates a class (remember, that a class is a table in MT OOP), and if some malicious code modifies that publicly available class's methods, any object that calls those methods calls the new, hacked method. That's messy. With functional OOP, the class cannot be modified. Go ahead. Try to think of a way to modify the class without completely replacing it. It can't be done.
local classes = setmetatable({}, {__mode = "k"}) -- Allow the GC to empty this as needed.
function class(f, super)
classes[f] = {super = super} -- store super in a table so that I can extend the data for a class later if I need to.
end
function new(f, obj, ...)
local fenv = getfenv(f)
if type(obj) ~= "table" then
error("bad argument: expected table, got " .. type(obj) , 2)
end
if classes[f] and classes[f].super then
new(classes[f].super, obj, ...)
local super = obj
obj = setmetatable({}, { __index = super })
obj.super = super
else
setmetatable(obj,{__index = fenv})
end
obj.this = obj
setfenv(f, obj)
f( ... )
setfenv(f, fenv)
return obj
end
function import(tbl)
local env = getfenv(2)
for k,v in pairs(tbl) do
env[k] = v
end
end
There's three functions here. class, new, and import. Class allows you to create classes with super classes. New creates an object from a class. Import is just a handy little function I made that imports a table into the environment of the calling function. Allow me to demonstrate the use of all these by an example API file and an example program. Then I'll explain
- API file "MyAPI"
function MyClass(arg1, arg2)
local privateVar = arg1
publicVar = arg2
function getPrivateVar()
return privateVar
end
end
import(MyAPI)
import(classAPI)
function MySubclass(arg1, arg2, arg3)
local privateVar = arg3
function test()
print(getPrivateVar() .. " " .. publicVar .. " " .. privateVar .. " " .. autoInitializedVar)
end
end
class(MySubclass, MyClass)
local obj = new(MySubclass, {autoInitializedVar="how are you?"}, "hey", "buddy", "boy")
obj.test()
print(obj.publicVar)
print(obj.privateVar)
print(obj.autoInitializedVar)
The output of this program should be
hey buddy boy how are you?
buddy
how are you?
Let's look at how it works. First, let's look at the MyAPI.MyAPI has one function called MyClass. You'll notice that in my class API, you don't need to use any kind of a "create class" function to make a class unless you want to subclass. You'll also notice that public variables of an object can be declared the way you would normally declare a global variable from a function, as made evident by the publicVar. This is because when an object is being created, it sets the class's environment to the object itself. So any globals declared are stored in the object table as public instance variables.
MyClass has one method called getPrivateVariable. This allows external code to get the private variable from the object.
The program file starts out with that import function. import(MyAPI). This takes the MyAPI table and puts all of its functions/variables into the environment of the caller. In this case, that means that I don't have to use MyAPI.MyClass to reference the class. I can just use MyClass. It also imports the classAPI so that I can call class and new without the class api name.
MySubclass also has a private variable with the name privateVar. This is mostly there to demonstrate that private variables in a subclass will not clash with private variables from a superclass.
Next it has the test function. This merely prints out all the data we've got, including an autoInitializedVar variable. I'll explain this one shortly. But do notice that from within the class, I can get any of an object's public data (including methods like getPrivateVar) without dot notation. This is because the previously mentioned environment trick.
Next, the class api is told that MySubclass is subclass of MyClass.
Now we create an instance of MySubclass with new(class, pre-existingObject, arguments…). This is where we define autoInitializedVar. the pre-existingObject argument is the table that gets turned into an object. So any data in that table becomes a public variable in the object. The arguments are passed as the class's parameters.
Next we call obj.test(). This gets the superclass's privateVar and publicVar, the subclass's privateVar, and the autoInitializedVar, concatenates them into one string and prints it out. Then we print(obj.publicVar). This prints "buddy" because that variable was declared public. Next we try to print(obj.privateVar) but get a nil output because private variables cannot be accessed outside the class. Then we print(obj.autoInitializedVar) just to prove that that variable is no different than normal public variables.
So that's how to use that class API. What about how the class API works? It's very simple.
There is a classes table declared locally that stores known classes and their superclass. (super is in a table for the sake of expandability in case I decide to add more to what a class can have). The classes are used as the key for easy access. Because of this, we want to make classes a weak table with the key ("k") as the weak part. This means that whenever the garbage collector runs, if that table is the only thing holding onto the class, it lets go of it.
The class function just inserts a class into the classes table.
The new function generates an object from a class. The class is run, and because its environment is set to the object, any globals declared are made as entries to the object table. That is the major reason behind why this API works.
The import function just gets the environment it was called from and imports all of the table's entries to the environment.
Hopefully other people see the usefulness of this method of OOP. It's more secure, it's got more features, and it has a simpler class API. Enjoy!
- Elvish out!