[Date Prev][Date Next][Thread Prev][Thread Next]
[Date Index]
[Thread Index]
- Subject: Object-oriented programming in Lua
- From: Mark Hamburg <mhamburg@...>
- Date: Sun, 16 Jan 2005 20:24:46 -0800
Here is my review of Lua with regards to the key elements of object-oriented
programming. Object-oriented programming can be seen as emphasizing:
* Encapsulation: Objects are only changed through controlled interfaces. If
you preserve the interface (and its semantics), you can change the
implementation without clients having to change.
* Polymorphism: The same interface can work with multiple types of objects.
* Inheritance: New types of objects can be defined based on existing types.
In the case of a prototype-based system, type and instance get somewhat
deeply intermixed.
This is my list, but it's reasonably standard. For example, it matches with
the Wikipedia (http://en.wikipedia.org/wiki/Object-oriented) while ignoring
the comments about emphasizing objects and abstraction which don't seem
particularly concrete in judging linguistic support.
Polymorphism
-------------
Lua does very well with respect to polymorphism. Given method lookup and
__index and __newindex metamethods, one can implement the following
interfaces in any number of ways and the client need never be any the wiser:
obj:method( ... )
obj.field
obj.field = expression
Encasulation
------------
With respect to encapsulation, Lua fares less well. If one really wants to
keep things hidden, it takes a lot of work with proxy tables and complicates
the object implementation. This may not seem like a big deal, but
encapsulation can make a big difference when analyzing systems. What we are
left with is programming by convention. For example, we can consider fields
with names starting with underscores to be private and not access them. This
may be sufficient if one is sufficiently trusting. If dealing with untrusted
code, however, Lua makes it relatively difficult to expose the interfaces to
objects without exposing their internals.
With a certain amount of memory expense, one could wrap everything with a
collection of object-specific closures via something like:
function makeOpaqueProxy( obj, ... )
-- Make an opaque proxy for the object.
-- ... Names the methods to export.
local result = {}
local count = select( "#", ... )
for idx = 1, count do
local name = select( idx, ... )
local method = name and obj[ name ]
if method then
result[ name ] = function( proxy, ... )
method( obj, ... )
end
end
end
return result
end
[ Side question: Would one be better off capturing ... into a table? Do we
need a varags.foreach( func, ... ) which calls func for each value in ...? ]
There is also the option of gaining encapsulation by wrapping the
object-state variables in a function and taking the method name as a
parameter (see PiL, pp 144-145.) This, however, fails to integrate well with
the conventional object-oriented syntax and would require a certain amount
of proxy table machinery to hide.
Inheritance
-----------
Lua works reasonably well for implementing both class-based and
prototype-based inheritance via metatables with one major caveat: There is
no easy support for accessing superclass methods. Now, arguably, it's no
worse than C++ where one needs to explicitly specify the superclass in any
such calls, but this further complicates class-based systems with respect to
what needs to be exported and how.
Sugar
-----
Somewhat unrelated in my opinion to the issues above are issues of
syntactic-sugar and other niceties. These clearly fall on a continuum with
some of the issues above since I complained about convoluted solutions, but
those convoluted solutions generally incur a runtime as well as a
programming time hit. The chief sugar issues I've seen raised are:
* The need for an explicit self within the code. This doesn't bother me.
Oberon-2 had it as well. It makes it easier to tell what is being referred
to in the code and reminds one that a table access will be taking place.
* : v . The issue here is that it would be perfectly legitimate for a
field to contain a function, so there isn't a good way to tell whether the
first parameter should be a hidden object reference if we use a period for
both field access and method calls.
We could avoid the issue by making the obj.method return a closure binding
the object and method together, but that gets expensive.
We could avoid the issue through semantic rules that would treat x.y
differently in call and non-call sites (something that already happens for
assignment), but that would generate convoluted code to call function values
stored in fields since we would need to read the value separately from the
call.
But the issue doesn't seem worth all of the effort. obj:method( ... ) is
simply the syntax for method calls.
What does seem problematic is that obj.method( ... ) does not generate an
error unless the first parameter happens to get used in a way that triggers
some other error. There are a variety of potential solutions including:
* Add a __methods metatable entry or extend __index to take a parameter
indicating that the lookup is for a method access. This would allow one to
make the method available only to method calls and not field access. This is
straightforward in the VM once one works out the desired semantics. Or...
* Introduce a new declaration "method" which would parallel function but
would be checked as only being allowed for use in a method call context. I
haven't looked at how easy this would be to check in the VM.
So, my overall marks on Lua with respect to "traditional" object-oriented
programming:
* Polymorphism: Very good
* Encapsulation: Difficult or expensive other than by convention.
* Inheritance: Straightforward for simple overriding. Awkward if one needs
to call inherited methods.
* Syntax: Reasonably good with the exception of the danger of not catching
obj.method( ... ) errors early.
Lua also makes it reasonable to implement less traditional object-oriented
approaches such as generic functions though it provides no particular
syntactic support for this. (I actually rather like generic functions.)
Mark