One of the things I’ve found slightly frustrating about Ruby is the new
method.
Nearly twenty years ago I was writing code in Objective-C on NeXT, which had explicit allocation and initialization:
Using this invocation, foo
could be any sort of Object, and init
could return any sort of Object.
This two-part object creation could do something out-of-the-box ruby couldn’t.
It’s easy to create a new object in Ruby.
An instance of a class Foo
is created by calling its new
method.
When new
is called it routes to the Foo
’s new instance method.
The code for the new method is written in C: the rb_class_new_instance
function is the new instance method in Foo
, and is mapped up into Ruby by the rb_define_method
function.
In the context of the Foo
instantiation above, this code allocates a Foo
instance, initializes it with whatever arguments were passed to new
, and finally returns the new object.
Something Different
Sometimes though, a new object is not what is really wanted. Sometimes an object may already exist that should be returned instead, or perhaps an object of another type should be created and returned. Whatever the reason and for whatever purpose, this is not possible using the standard Ruby mechanism - only the allocated instance is returned. To return an object different from the one allocated, an enhanced mechanism is needed.
In a class that uses Class’ new method to construct instances, there are only two calls that may be intercepted to enhance its functionality - memory allocation and initialization.
During memory allocation the space needed to hold an instance is allocated by Class’ allocate
method, which returns an uninitialized instance of the class.
During initialization the instance’s initialize
method is called using any arguments that were passed to new
to set up the initial state of the instance.
It’s the initialize
method’s responsibility to call its superclass’ initialize
method - which is responsible for calling its superclass’ initialize
and so on up the chain to Object, from which all instances derive.
The only other constraint on any mechanism affecting object creation is that it should be above the surface of the Ruby interpreter - object creation should be extended, not replaced in Ruby.
Returning an Alternate Object
Initialization is the more reasonable place to control the enhanced functionality since allocation is generally logic-free.
This also allows information in a partially instantiated instance to be used to make decisions about its possible replacement.
When initialize
determines that another object should be returned by new
, some sort of replacement mechanism must be used.
Looking back at the C code, it is allocation, not initialization, that determines the object that is returned by new
.
Since the value returned from initialize
is ignored and the Ruby Interpreter cannot be changed, different semantics are required to replace an object.
A remap_new_object
method that can be called by initialize
to declare an object replacement works well.
After moving the original new
method aside with an alias
, a new new
method is defined that first calls the original new
method and then either returns the alternate object that was reassigned, or the object itself.
If an alternate object is present, it’s deleted upon retrieval to keep the remapping Hash small.
This would work fine if some classes didn’t already redefine the new
method.
The enhanced code above can’t deal with this situation since these classes may not even call Class’ new
method.
Rather than intercepting the new
and possibly returning an alternate object, these classes would bypass the remapping code altogether.
The mechanism must still be adjusted somewhat to handle this.
Elective Alternate Remapping
Part of the code is still good: the chunk that creates the Hash and adds alternate objects to it.
What’s still left is the retrieval of an alternate object.
Thinking more about the whole idea of returning alternate objects from new, this relatively small change is philosophically a radical break from standard Ruby. It probably makes sense to require classes that need object replacement during creation to explicitly include this capability. Only when a class elects to have the capability should the mechanism to alias the new method and lookup remappings be put into place - for just that class. Besides positively asserting the class’ intention to break from standard Ruby, this explicitness will help safeguard against cavalier use of such non-standard behavior.
The code to do this is the virtually same as was in the Class version:
To use this, a call to allows_object_remapping_in_new
would be made by each class that needed remapping capability.
By making the call, the metacode would add the retrieval mechanism to the class.
Note the metacode is surrounded by the class<<self ... end
construct.
This is required because it must be evaluated at the class level rather than the instance level.
Ruby is intertwingled at this point: the instance methods of Class are the class methods of Class’ instances (the classes themselves.)
This is what allows Ruby to be entirely object-oriented - it folds back on itself in a strange loop.
An Example
Consider a simple class Foo
that either creates an instance of itself or an instance of another class Bar
when new
is called.
(The criteria for deciding which to create is purposefully trivial here, but should demonstrate the idea - much more complicated logic can be used as the situation dictates.)
When this code runs, it produces:
Simple and straightforward.
Also, if Bar
conditionally created and returned an instance of some other class, say Mumble
, Bar
’s initialize
method would remap the allocated Bar
instance to the Mumble
instance.
Then the Foo
instance would remap to the Mumble
instance and it would ultimately be what was returned from Foo
’s new
.
Cascaded remappings like this require no additional effort or special knowledge outside the initialize
method; multiple levels of alternative remapping happen naturally.
Last Words
So now there’s a generic Ruby way for object creation to return a different object than the one allocated.
However, care must be taken in the use of this mechanism, lest havoc ensue.
Generally, if the object returned from new
is remapped, the returned object should either be of the same type, or have duck-like aspects of the original where needed.
If what is returned is not compatible, any manner of problems may take place.
This is likely the reason that the semantics of standard Ruby’s new method are what they are.
Careful (and perhaps exhaustive) testing of any code that uses this mechanism should definitely take place!
The mechanism has been added to the eymiha rubygem, available at rubygems.org.