Copyright (C)
2011 by Steve Litt
Contents
Introduction
Does Lua have OOP?
I guarantee you no matter whether you answer that question "yes" or "no", you will get yelled at by Lua devotees.
"No!", exclaim some purists. "Lua isn't locked into a single paradigm
like object orientation. Lua has no syntax for classes or objects."
they continue. The purists then go on to say Lua gives you ways that
*you* can construct things that act like OOP in other languages.
The purists are right.
"Yes!" exclaim some pragmatists. "Every major OOP thing you can do in
other languages you can do in Lua. Encapsulation, polymorphism,
inheritance, Lua does it all. The only difference is in Lua you can do
it in many different ways, but you can do object oriented programming
in Lua, so Lua is OOP." The pragmatists then go on to say Lua gives you
ways that *you* can construct things that act like OOP in other
languages.
The pragmatists are right.
And then there are the guys in the middle to whom whether or not Lua is
OOP is just a marketing label anyway. We stay out of that particular
argument. We just use Lua to do the OOP idioms we've done in other
languages, and probably do it easier. And because there's no specific
Lua OOP syntax, we construct our own objects out of tables, closures
and metatables. And because we so construct them, we truly understand what's in them, kind of like when you build a computer from parts you understand what's in it.
And you know what that understanding means? It means you'll never forget the syntax for your classes and objects, because you constructed them from basic Lua constructs.
There are many ways to construct and use objects in Lua. Many are too
advanced for Litt's Lua Laboratory, at least on this page. They use
metatables, and we haven't covered those yet.
So on this page I'll show you one method using closures and tables but
not metatables. The method I'll show you has encapsulation but not
polymorphism nor inheritance. This page's method has "private"
properties accessed by setters, getters and other "methods". I put
quotes around "private" and "methods" because these are other
languages' terms for these things, not Lua's.
So kick back, relax, and learn how we do OOP or Pseudo-OOP in Lua...
Dots and Colons
On this page you'll see OOP methods run like this:
objectname.methodname(args)
And yet in a lot of other code you'll see it like this:
objectname:methodname(args)
Notice one has a dot and one has a colon. Why the difference?
It becomes obvious when you consider that objectname:methodname(args)
is just syntactic sugar for objectname.methodname(objectname, args).
Most people write Lua classes and objects such that you need to
actually pass the object as the first argument to the method. Since
it's both time consuming and amateurish to write the object name twice,
the colon syntax was developed so you wouldn't have to.
If you write code like I did, using closures not requiring the object
name as an argument, be sure to argument your code so other programmers
know why you used a dot instead of a colon.
Hello World: The Point Class
A point is a thing with an x measurement and a y measurement. You need
to set each and get each. In GUI applications points have methods to
draw and undraw themselves, but what we're doing isn't GUI. We'll make
a "method" called show()
that simply prints out the x and y values. The "constructor" sets x and
y, or if either doesn't exist it's 0. Here's a review of the methods
for the Point class we'll be developing:
- new(x, y)
- setx(x)
- sety(y)
- getx()
- gety()
- show()
As you look at this code, remember the class is made with a closure,
and that functions inside of other functions can see local variables of
the outer function. The outer function is Point.new(). One of that outer function's local variable is a table called self, which is what is eventually returned by Point.new(). That returned local variable, self, is the object returned by the class's constructor.
Here's the code:
#!/usr/bin/lua
Point = {} -- THE CLASS
Point.new = function(x, y)
-- #PRIVATE VARIABLES
local self = {} -- Object to return
x = x or 0 -- Default if nil
y = y or 0 -- Default if nil
-- #GETTERS
self.getx = function() return x end
self.gety = function() return y end
-- #SETTERS
self.setx = function(arg) x = arg end
self.sety = function(arg) y = arg end
-- #OTHER METHODS
self.show = function(msg)
print(string.format("%s (x,y)=(%d,%d)",
msg, x, y))
end
return self --VERY IMPORTANT, RETURN ALL THE METHODS!
end
mypoint = Point.new(2,5)
mypoint.show("Before tweaking:")
--NOW DOUBLE BOTH X AND Y
mypoint.setx(2 * mypoint.getx())
mypoint.sety(2 * mypoint.gety())
mypoint.show("After doubling:")
The preceding program produces the following output:
slitt@mydesk:~$ ./test.lua
Before tweaking: (x,y)=(2,5)
After doubling: (x,y)=(4,10)
slitt@mydesk:~$
Take five minutes to examine and run the code. Don't examine it for
understanding -- we're going to discuss it line by line. Examine it for
its clean simplicity. Say goodbye to Perl's silly bless command. I've been doing Perl since 1995 and I still don't understand the bless command. Say goodbye to the syntactical typing nightmare of C++.
Maybe you consider type checking a good thing. OK fine, use assert()
functions against the types of x and y in the setters and the
constructor. No big deal. You'll probably want to do that if you're
writing classes for other people to use. But personally, speaking for
myself, I'm glad typechecking is optional, so if I'm writing something
quick and dirty, and I know how to use it, typechecking doesn't get in
my way.
Now let's examine the code line by line...
Line by Line Analysis
CODE
|
EXPLANATION
|
Point = {} -- THE CLASS
|
This is a table that will contain the new() function so that it can be called Point.new(). and resemble the class/object relationship in other languages. Actually, if you called the constructor Pointnew() instead of Point.new(), you wouldn't even need this table. But we use it to get that familiar OOP look.
|
Point.new = function(x, y)
|
Constructor function, just what
you'd expect. You pass in x and y to give the point a location. The
purpose of a constructor is to pass back an object, and that's just
what Point.new() does. Read on...
|
local self = {} -- Object to return
|
This is a little tricky. In OOP
speak this is the object that will be returned by the constructor. But
from a Lua perspective this is just a table to contain all the
functions inside Point.new(). The functions and NOT the variables. You know why? The variables x and y are local variables of the outer function, Point.new()-- they are not in self. But the functions are in self,
and since an inner function can see its outer function's local
variables (this is the basis of closures), the functions inside self can retrieve and manipulate the local variables for Point.new().
So from a Lua point of view, self is a bag into which to put all the functions declared by Point.new().
|
x = x or 0 -- Default if nil y = y or 0 -- Default if nil
|
There's a lot of Lua in these two simple lines. First of all, a widely used Lua idiom for defaulting a variable looks like this:
a = a or a_default
This is short circuit logic. If a exists then it's set to itself. But if it doesn't exist (perhaps no argument passed into the function), then it's set to a_default.
But even beyond this, remember that in Lua, a functions arguments
function EXACTLY like local variables within that function. Therefore, x and y are visible and manipulable to all functions inside of Point.new().
|
self.getx = function() return x end self.gety = function() return y end
|
Getter functions. Put inside table self.
Remember that x and y are local to the outer enclosing function
(because they're arguments of that function), and therefore are
available to the inner function. This is how closures work.
|
self.setx = function(arg) x = arg end self.sety = function(arg) y = arg end
|
Setter function. Put inside table self. Same explanation of x and y and closures.
|
self.show = function(msg) print(string.format("%s (x,y)=(%d,%d)", msg, x, y)) end
|
Pretty print's x and y with a message passed in as an argument. Put inside table self. Same explanation of x and y and closures. |
return self
|
By the end of Point.new() , self is a bag of functions containing getx, gety, setx, sety and show. By passing this back, the calling program can execute all these functions. If you forget to return self then you'll get an error message something like this:
slitt@mydesk:~$ ./test.lua
/usr/bin/lua: ./test.lua:29: attempt to index global 'mypoint' (a nil value)
stack traceback:
./test.lua:29: in main chunk
[C]: ?
slitt@mydesk:~$
|
end
|
This ends Point.new(), the constructor.
|
-- BELOW HERE IS THE MAIN PGM
|
|
mypoint = Point.new(2,5)
|
OOP viewpoint: Use class Point to instantiate object mypoint.
Lua viewpoint: Use maker Point.new() to deliver a table of functions to variable mypoint, with state variables held by the closure enclosed by Point.new().
|
mypoint.show("Before tweaking:")
|
Pretty-print x and y with a message.
|
mypoint.setx(2 * mypoint.getx()) mypoint.sety(2 * mypoint.gety())
|
Set x to double the value returned by getx(). Same with y.
|
mypoint.show("After doubling:")
|
Pretty print x and y with message, proving that each has been doubled.
|
Review this entire article, especially the line by line table of
explanations, until you understand just what happened here and why. See
if you can appreciate the simplicity and logic of the way Lua has been
used to implement what in other languages would be called OOP, and
notice that in the main program the syntax looks like most other
languages' OOP implementations.
One Class, Multiple Independent Objects
The preceding article instantiated only one object. You might be
thinking that if you instantiate and then manipulate multiple objects,
the objects would interfere with each other. They don't, as this
article proves.
From the Point Class article, leave the Point class code as is, but replace its main routine with the following:
point1 = Point.new(1, 2)
point2 = Point.new(3, 4)
point1.show("Point 1 before tweaking:")
point2.show("Point 2 before tweaking:")
point1.setx(8)
point1.sety(9)
point1.show("Point 1 after setting point 1 to (8,9):")
point2.show("Point 2 after setting point 1 to (8,9):")
point2.setx(5)
point2.sety(6)
point1.show("Point 1 after setting point 2 to (5,6):")
point2.show("Point 2 after setting point 2 to (5,6):")
What was done was two points were instantiated and shown. Then point 1
was tweaked and the show statements should indicate that point 1 has
changed but point 2 remains the same. Then point 2 was tweaked and the
show statements should indicate that point 1 remains the same as it was
after you tweaked it.
Now let's look at the output:
slitt@mydesk:~$ ./test.lua
Point 1 before tweaking: (x,y)=(1,2)
Point 2 before tweaking: (x,y)=(3,4)
Point 1 after setting point 1 to (8,9): (x,y)=(8,9)
Point 2 after setting point 1 to (8,9): (x,y)=(3,4)
Point 1 after setting point 2 to (5,6): (x,y)=(8,9)
Point 2 after setting point 2 to (5,6): (x,y)=(5,6)
slitt@mydesk:~$
Bang! Modifying one does nothing to the other. They're independent, just like objects should be.
About Class Variables
Some OOP implementations have "Class Variables", which are shared
across all objects of a given class. Seems like asking for trouble to
me, but if you really need it I'm sure there's a way to do it within
Lua.
Example: Distance Between Points
We can find distance between two points with the addition of one more function. Call it distance_to(), which takes another Point object as an argument and returns a table with the following keys:
- xto
- yto
- absdistance
- angle
All the preceding are from the point of reference of the point running the function, not the point used as an argument. Therefore if the original point is (5,5) and the argument point is (0,0), then xto would be -5, yto would be -5, absdistance would be 5 * sqrt(2), and angle would be 225.
In this case typechecking is more important, because you want to make
sure it's passed a real point and not something else. So what you'll do
first is check that the argument is of type table (remember, the
objects returned by the constructor functions on this page were tables.
But beyond that, we'll check that the argument has elements called distance_to, new, getx, gety, setx and sety. If all those check out, it's very probably another point object.
So to the Point class in the Point Class article, add the following method:
self.distance_to = function(pnt)
if type(pnt) == "table" and
pnt.getx and
pnt.gety and
pnt.setx and
pnt.sety and
pnt.distance_to then
else
io.stderr:write("ERROR: Argument to Point.distance_to() must be another Point.\n")
io.stderr:write("Aborting...\n\n")
os.exit(1)
end
local rtrn = {} -- Table to be returned
rtrn.xto = pnt.getx() - x
rtrn.yto = pnt.gety() - y
rtrn.absdistance = math.sqrt(rtrn.xto * rtrn.xto + rtrn.yto * rtrn.yto)
rtrn.angle = math.atan2(rtrn.yto, rtrn.xto) * 180/math.pi
return rtrn
end
The first if/else tests that the argument is truly a Point object. The
next few statements build a table of quantities representing the
distance from the current point to the argument Point, and then you
return that table. Then write the following main-routine code to
exercise the new method:
local point1 = Point.new(0, 0)
local point2 = Point.new(4, 4)
local d = point1.distance_to(point2)
print(string.format("d.xto = %f", d.xto))
print(string.format("d.yto = %f", d.yto))
print(string.format("d.absdistance = %f", d.absdistance))
print(string.format("d.angle = %f\n", d.angle))
local point1 = Point.new(0, 3)
local point2 = Point.new(4, 0)
local d = point1.distance_to(point2)
print(string.format("d.xto = %f", d.xto))
print(string.format("d.yto = %f", d.yto))
print(string.format("d.absdistance = %f", d.absdistance))
print(string.format("d.angle = %f\n", d.angle))
The preceding code puts point1 at the origin and point2 at (4,4), so it's obvious to get from point1 to point2
you'd need to advance +4 in each direction, that absolute
distance is 4*sqrt(2), and the angle is 45 degrees. Then it
runs again with point 1 at (0,3) and point 2 at (4, 0), obviously a
3-4-5 right triangle. Starting at point 1 you'd have to advance +4 to
get to point 2, and you'd have to advance -3 to get to point 2. Since
it's a 3-4-5 right triangle the absolute distance should be 5, and if
you visualize the line going from (0,3) to (4,0) that line is somewhere
between 0 and -45 degrees. Run the code and you get the following
output:
slitt@mydesk:~$ ./test.lua
d.xto = 4.000000
d.yto = 4.000000
d.absdistance = 5.656854
d.angle = 45.000000
d.xto = 4.000000
d.yto = -3.000000
d.absdistance = 5.000000
d.angle = -36.869898
slitt@mydesk:~$
Precisely what you expected.
Preventing Direct Property Manipulation
The preceding several articles are how I write my Lua objects, and it
works just fine for me. But there's a potential for mistakes because an
applications programmer naive to the exact way you wrote your objects
might think he could manipulate x by directly setting point1.x rather than using point1.setx().
Lua would go merrily along letting him do that, but if he used both,
the results would be inconsistent because they manipulate two different
variables. If you remember, the Point class from the last few articles
returns a table of functions that can be used as an object. If an
applications programmer sets point1.x, he simply adds a new element to the table, an element called x. As you remember, point1.setx() really sets a closure variable, not an element of a table. Two different things.
If you really, really really
want to prevent all access to anything within point1 except its
methods, you can lock it down with the following code inserted just
before returning self in Point.new():
--#### PREVENT READ AND WRITE ACCESS TO THE RETURNED TABLE
local mt = self
-- PREVENT WRITE ACCESS AND ABORT APPROPRIATELY
mt.__newindex = function(table, key, value)
local msg = string.format("%s %s %s %s %s",
"Attempt to illegally set Point object key",
tostring(key),
"to value",
tostring(value),
", aborting...\n\n"
)
io.stderr:write(msg)
os.exit(1)
end
-- PREVENT READ ACCESS AND ABORT APPROPRIATELY
mt.__index = function(table, key)
if type(key) ~= "function" then
io.stderr:write("Attempt to illegally read attribute " ..
tostring(key) .. " from Point object, aborting...\n\n")
os.exit(1)
end
end
-- WRITE NEW __index AND __newindex TO METATABLE
setmetatable(self, mt)
The preceding uses metatables, which I promised not to get into, and as
far as I'm concerned it's over the top overkill. But if you for some
reason want to program Lua with the same anal retentive encapsulation
enforcement as C++, now you have a way to do it.
Discussion: Polymorphism and Inheritance
When Philippe Kahn of Borland Software taught me OOP in a video
featuring himself, his flute and his car, he taught me that OOP had
three properties:
- Encapsulation
- Polymorphism
- Inheritance
The Point class described on this web page, especially if augmented with the __index and __newindex mode in the Preventing Direct Property Manipulation article, enforces encapsulation about as far as a reasonable person would want to enforce it.
Polymorphism is where a function acts differently depending on what
it's acting on. Due to Lua's loose type checking, a lot of the
polymorphism you need in C++ or Java just to get around their
ubiquitous type checking is unnecessary. The rest can be done with
metatables and is beyond the scope of this web page.
Inheritance is where you define a new class as being just like an
existing class except that it also has this and that and the other,
where you define this, that and the other. Anything not defined as
being a difference is run just like the old class (the base class). In
Lua you can achieve this with metatables, but once again it's beyond
the scope of this web page.
Discussion: Many Ways to Do OOP
You know it's funny. In C++ or Java there's basically one way to do
OOP. It's built solidly into the language. It's cast in concrete.
Not so with Lua. Lua wasn't built with OOP in mind, it was built with
atomic computer programming features such as closures, iterators,
tables, metatables, tail recursion. The programmer can then easily use
those atomic features to create OOP, along with whole bunches of other
programming paradigms. The programmer can build OOP many different ways.
It's funny. My biggest disappointment with Perl was Larry Wall's "there
are many ways" philosophy. And yet with Lua I accept that same
philosophy. Except it's not really the same. Unlike Perl, Lua has a
*very* small set of syntax. And a *very* consistent set of syntax. It's
just that you can use that syntax to do all sorts of things.
For the person who believes to the bottom of his soul that OOP is the
be all and end all of computer programming, Lua is probably not the
right language. C++, Java, Ruby or Smalltalk would probably be better
for such a person. But for the person who believes OOP is one tool of
many powerful tools a programming language can and should offer, Lua's
the choice.
[ Troubleshooters.com|
Code Corner | Email Steve Litt
]
Copyright
(C) 2011 by Steve Litt --Legal