Insights into Ruby from Lua
In my job, I have to use Lua to program some of our software. For anything complex, I love OOP concepts. For those that don’t know, Lua doesn’t have OOP, though it has some syntactic sugar to make basic OOP possible.
So, I wrote a little OOP library based on Ruby’s object model. I created an Object
“class”, and a Class
“class”, and Class
is an instance of an Object
, and instances of Class
are themselves objects that create instance that inherit from the class on up to the Object
prototype. And so on. (Once I got my head around Lua concepts, it’s remarkable how easy it was. Only 99 lines of Lua code.)
In the process, I tried to make a OOP system that was better than Ruby’s, by changing two things that have always bothered me. In both cases, I failed; the reasons why I failed were both “Ah-HA!” moments for me that gave me insight (I think) into why Ruby is designed as it is. I share them with you below.
new
versus initialize
I was always a little bothered by the fact that in Ruby when you call new
on a class, you define an initialize
function to handle it. I mean…wtf, why not just call what you write new
instead? I was also annoyed that if you return a specific value from your initialize
method it gets ignored.
I decided that I’d have the user actually write their own new
function for the class, and use its return value. In implementation, it
looked something like this:
Rectangle = AKClass:new( ) function Rectangle:new( height, width ) local theInstance = AKObject:new( self ) theInstance.height = height theInstance.width = width return theInstance end
As I wrote code like that again and again, I realized that in every new
function I had to call AKObject:new( self )
to create the
instance, and then return it at the end. I realized that if every class inherited it’s new
from AKObject
, then I could just delegate that
work to AKObject
, and let the class just define what to do to the instance. Suddenly, I had Ruby again, and (now that I’d seen the
alternative) I liked it:
function AKObject.new( owningClass, ... ) local theInstance = { -- Lua stuff for setting up the inheritance here } if type( owningClass.initialize ) == 'function' then owningClass.initialize( theInstance, unpack( arg ) ) -- No point in using the return value here end return theInstance end function Rectangle:initialize( height, width ) self.height = height self.width = width end
The Trouble with First-Class Functions
I have repeatedly complained on the ruby-talk mailing list about the fact that Methods aren’t first-class functions. (The term ‘first-class functions’ as I’m using it here is another way of saying ‘function literals’: it means that functions are just another atomic variable type, that can be invoked with any object as the ‘self’ scope.) Blocks and Procs and Methods are all different? Bleah! Special-cases are the opposite of elegant.
Lua is nothing but first-class functions, so I was happy…until I tried to write a super
method to call the method with the same name on the parent object. Inside a function, I have no idea what that function is called. So I then tried to write a special-case (urgh!) superinit
method specifically for calling parent initializer…and that failed. I finally got to the core of the problem this morning:
function Rectangle:initialize( width, height ) self.width = width self.height = height end function Square:initialize( size ) self.class.superclass.initialize( self, size, size ) end function UnitSquare:initialize( ) self.class.superclass.initialize( self, 1 ) end
(For those not familiar with Lua, the colon used when defining a function means “Hey, please create an implicit first parameter named self
because I’m too lazy to type it each time.” This is why I can pass the self
from the subclasses to the parent method, and have the initializer operate on it instead.)
The above works just fine when I create a new Square, but I get some infinite recursion when I try to create a new UnitSquare. Here’s an English trace of what’s happening:
-
AKObject
creates a new instance ofUnitSquare
, and passes that instance to theUnitSquare
initializer. -
UnitSquare
’s initializer finds the class of the supplied object (UnitSquare
), its parent class (Square
), and calls that class’sinitialize
function, passing along theUnitSquare
instance. -
Square’s initializer finds the class of the supplied object (
UnitSquare
), it’s parent class (Square
), and…oh hell, we’re stuck in a loop.
The problem is that when I wrote “self.class.superclass” what I really meant was “Hey, I want the superclass of the class that owns this method, not the object that I happen to be operating on.”
Which means that methods need to be associated with a class.
Which means that they’re not first-class functions.
Aw fuck. Matz is smart. :)