Submitted by transami (Sun Oct 16 20:34:30 UTC 2005)
The work herein is the culmination of multi-year discussion and inquiry on the topic of AOP for Ruby. It has been carried-out with the ultimate hope of establishing Ruby as a premier AOP language, indeed the AOP language of choice. Since AOP is a very powerful paradigm for abstracting programming solutions into separate concerns, and shows great promise for improvements in code maintenance and reusability, it seems only natural that an agile language such as Ruby would provide support for this increasingly popular pattern of design.
In AOP, one considers aspects of concern applicable across multiple classes and methods. Thus AOP is said to address cross-cutting concerns. Aspects consist of advice, which are methods designed to intercept other methods or events according to specified criteria. This criteria is called a point-cut and it designates a set of join-points. A join-point (or code-point) is the specific place within a program's execution where the advice can be inserted. In this way, AOP is thought to provide a means of organizing code orthogonal to OOP techniques.
^ | OOP | Prob Set. | +-------------> AOP
The overall concept is very powerful, but likewise it can be difficult to integrate into an underlying system, easily succumbing to limitations in efficiency and subverting the intended ease-of-use and reusability. For these reasons we believe AOP has not yet become widespread. Our design addresses these issues.
To qualify as an AOP capable language, the following criteria must be given considerable support:
The above four points are the functional criteria of any implementation of AOP. In addition there are three major means of implementation:
While the capabilities of these basis largely overlap, they admit of enough distinctions to justify independent support in accordance to the needs of the language. The first of these is generally ill suited to a highly dynamic language like Ruby (although we have recently determined that a hybrid of the first and last may be feasible), and Ruby already has some support for the third basis, albeit limited, via set_trace_func, but Ruby is hampered on the second count. This RCR focuses on the second basis, which is really the most suitable to a dynamic language like Ruby.
A mention before getting into the heart of this proposal: The development of this RCR has been guided by the following two important principles:
The first and foremost requirement of AOP is interception. A few years ago it occurred to us that subclassing itself is very similar to interception. The difference was merely a matter of the visibility of the subclass. With interception, the subclass needed to have its effect transparently. Indeed, Transparent subclassing is the fundamental proposition of this RCR. To accomplish it in Ruby we propose to introduce a new class called the Cut. A cut is a primitive unit of aspecting. It is used to encapsulate advice for a single class. Cuts are self-contained units much like classes and therefore can have their own state (introduction) as well as private auxiliary methods. Although the Cut class is very similar to the Class class, it cannot be instantiated. Rather it is used solely as an "invisible overrider". An example will help clarify.
Given a class C:
class C def m1(*args); 1; end def m2(*args); 2; end end
One would normally subclass C in order to gain new functionality.
class A < C def m1 print '{', super, '}' end end A.new.m1 #=> {1}
But unlike a regular subclass, a cut acts transparently. So we introduce the 'cut' construction as follows.
cut A < C def m1 print '{', super, '}' end end C.new.m1 #=> {1}
Now, even though we have instantiated class C, we have the functional equivalent of the subclass of C, namely A. Another way of saying this is that we have cut-across the behaviour of C with A. The cut is advantageous in its fine control of how advice interact with the intercepted class and its simple conformity to OOP design. By utilization of the cut AOP begins to flow naturally into ones programs.
Because the Cut is essentially Class, like a Class it can also be defined anonymously, either through instantiation or as a special singleton. The anonymous definition can be especially convenient for internal wraps; useful for assertion checks, temporary tests, etc.
class C
def m1; 9; end Cut.new(self) do def m1 '{' + super + '}' end end
end
C.new.m1 #=> {9}
Or through the special singleton form,
c = Object.new def c.m1; 8; end cut << c def m1 '{' + super + '}' end end c.m1 #=> {8}
Additionally, Cuts exist in proxy form to allow modules to be "premixed". This is analogous to proxy classes which allow modules to mixin to the class hierarchy. So too does a proxy-cut include a module, albeit preclusive rather the inclusive in its effect. We offer the module command #preclude to serve as designator of this purpose.
module A def m ; "<#{super}>" ; end end Class T preclude A def m ; "okay" ; end end T.new.m #=> "<okay>"
The Cut class is at the heart of this proposal. The remaining sections build on this basic device, demonstrating how to use it for AOP, and offers some important complementary suggestions to make Ruby more convenient with regard to it and AOP requirements in general.
A cut is useful for applying advice which intercept the methods of a single class. But to provide the full advantage of AOP we must also be able to cut-across multiple classes. The simplest means of cross-cutting is by use of a shared module. A shared module can serve as a simple aspect by its inclusion in a cut for each class.
class C def x; 'C'; end end class D def x; 'D'; end end module A def x '{' + super + '}' end end cut Ac < C ; include A ; end cut Ad < D ; include A ; end C.new.x #-> {C} D.new.x #-> {D}
Using a cut, advice intercept methods of the same name and use #super to call back to those methods --the basics of subclassing. But for advice to be fully reusable, a way to specify alternate method-to-advice mappings is needed. This can be done by calling secondary methods, as one might normally do within a class.
cut A < C def x bracket end def y bracket end def bracket '{' + super + '}' # PROBLEM! end end
But notice the problem that arises. Super will not be directed to m1
or m2
in class C, but to bracket
which isn't defined in C. This is not the desired result. A presently possible way to correct this is to pass a closure on the super call of the target method.
cut A < C def x bracket( lambda{super} ) end def y bracket( lambda{super} ) end def bracket( target ) '{' + target.call + '}' end end
This works well enough, but it is a rather brutish; nor does it provide any significant inspection. An improvement is to pass the target method itself, but enhanced to provide the current super context, and usefully, its own name.
cut A < C def m1 bracket( method(:m1) ) end def m2 bracket( method(:m2) ) end def bracket( target ) puts 'Advising #{target.name}...' '{' + target.super + '}' end end
In AOP, the above technique is such a common occurance, that it would be of benefit to further introduce a specail method for accessing the current running method, perhaps the word this
. With "this" in place the above example can be nicely simplified.
cut A < C def m1 bracket( this ) end def m2 bracket( this ) end def bracket( target ) puts 'Advising #{target.name}... '{' + target.super + '}' end end
In addition, since advising multiple methods with alternate advice, as we have done in the above example, is also such a common case of AOP, a helper method can make it even more convenient. For this we suggest a relatively trivial routine Module#redirect_advice.
cut A < C redirect_advice :m1 => :bracket, :m2 => :bracket def bracket( target ) '{' + target.super + '}' end end
There is an important issue to consider with using redirected advice or, more importantly, when using modules as reusable aspects: care must be taken in choosing method names so as not to inadvertently interfere with the methods of the class(es) being cut. This is problem because it inhibits code reuse, i.e. the ability to design components without regard to where they may be applied. For example:
class C def m ; "M" ; end def w ; "W" ; end def d ; w ; end end module MA def w( target ) '{' + target.super + '}' end end cut A < C include MA def m ; w( this ) ; end end C.new.d #=> "{W}"
In this case, #d does not return "W" as expected, but rather "{W}" because the advice in MA caused a name clash with the #w method in C. To fulfill the true abstraction and reusability potential of AOP this issue must be remedied. And it is a difficult issue that has been a great source of debate and deep consideration in determining how to provide a solution. It was actually David Black who provided the missing concept needed to form a final reasonable solution.
I think if you're inheriting from a class [or module] the burden is on you to know what that class's instance methods are. It's hard to imagine a case where it would be otherwise.
With this idea we have derived a suitable approach from Ruby's ability to dynamically manipulate class/module definitions on the fly, in other words "subclassing" the module and applying any required revisions to avoid unwanted name clash.
class C def m ; "M" ; end def w ; "W" ; end def d ; w ; end end module MA def w( target ) '{' + target.super + '}' end end module MArC include MA rename_method :q, :w end cut A < C include MArC def m ; q( this ) ; end end
This solves the problem in a very controllable way, which is nice. Yet it is somewhat long winded. To make it more convenient we can introduce a special form of include that anonymously does the revisions prior to inclusion.
cut A < C include_revision MA do rename_method :q, :w end def m ; q( this ) ; end end
With this solution, the reuse of independently defined aspect modules is readily doable.
Typical AOP systems are designed around system-wide effects via specialized join-points and global definitions of pointcuts. Our approach has purposefully avoided this, instead providing a direct "atomic" application of advice to individual classes ans methods, which in turn allows for a more efficient implementation. We believe this has many advantages over the prevalent top-down approaches. Yet we do not exclude the larger functionality either. Thanks to the reflexion Ruby grants by way of ObjectSpace, we can easily cut across large swaths of classes.
ObjectSpace.each_object(Class) { |c| if c.instance_methods(false).include?(:to_s) Cut.new(c) do def :to_s super.upcase + "!" end end end end "a lot of shouting for joy".to_s #=> "A LOT OF SHOUTING FOR JOY!"
There are plenty of great applications for broad cross-cuts like this, especially in the way of code inspection, unit testing, debugging, etc. However ObjectSpace does have some limitations. For instance, Fixnum, Symbol, NilClass TrueClass nor FalseClass actually belong to the ObjectSpace. While minor, we hope these limitation can be removed in the future to foster complete AOP coverage. In addition, some other methods may of use in this context, such as a list of #predecessors, analogous to #ancestors, or a method that mirrors #is_a?, and so on.
In contrast, the traditional approach taken by the most AOP systems today, largely propagated by early implementations like Aspect/J, have proven unwieldy and ironically end-up inhibiting code reuse. Infact, the limited reusabiliy has been speculated elsewhere as a potential primary culprit in the limited penetration of AOP to date. This proposal circumvents these issues by offering a general solution directly integrated into the OOP system, rather than attempting to operate wholly beyond it.
Cuts also trump simple method-wrapping mechanisms, like those proposed in Matz' RUbyConf 2004 presentation. While method hooks are especially convenient, they are weak with regards to SOC (Separation Of Concerns); most notably, method hooks lack introduction altogether. They also suffer from order of execution ambiguities that must be dealt with by imposing limitations or adding increasingly specialized declarations. Cuts again circumvent these issues by utilizing an inherent OOP construct --the subclass, rather than adding on a new, wholly "other" entity.
Another implementation detail to consider that falls outside the strict scope of this proposal, but that goes a long way toward bolstering it, is the limits on introduction due to the non-locality of instance variables and methods. Presently cuts will only be able to provide introduction through class varaibles --useful but weak by comparision. With the advent of locals in a future version of Ruby, cuts would gain robust introduction strengths.
The first real step in implementation is, of course, the creation of the transparent subclass, the Cut. This requires an addition in the structure of an object's class hierarchy; essentially a new pointer to a chain of cuts, the last pointing back at the cut class itself --a very simpleton explination to be sure. But fortunately, a well written Ruby patch has been coded by Peter Vanbroekhoven. It implements most of the core funtionality described here, and should serve as a means to investigate and test the potential utility of this RCR. It may also serve as a basis for including these AOP features into Ruby proper, should this RCR be accepted. The patch can be downloaded and found under "cut (transparent subclass)".
Comments | Current voting | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
|
RCRchive copyright © David Alan Black, 2003-2005.
Powered by .
Also, a thing I like about this proposal is that it is completely unintrusive (people who don't want to learn about AOP can just ignore Cuts, and everything else will just work as it used to) and doesn't need a preprocessor (unlike AspectJ for instance).
This RCR is well thought-out, researched and argued. A true work of art! I've gladly voted "strongly advocate".
Sorry, that was me speaking here above (Christophe Grandsire).
I am not opposed to AOP in Ruby. Nor am I opposed to the principle of "cuts" for the implementation of AOP in Ruby. I however voted opposed to the RCR in its current form due to a major flaw (in my mind) of the current implementation of cuts.
The flaw is this: a cut made on a base class is always inserted immediately under the base class in the inheritance heirarchy; that is, advice from the cut is always imposed between the aspected classes method and any redefinition/extension of that method in subclasses. In my view of AOP, it should be possible to have the advice atomically surround the method, no matter the depth below the actual cut point.
Take this (contrived) example:
In the current implementation, the advice is applied strictly to Foo#name, including the call to super from Bar#name. Result:
In some cases this is fine, but in other cases I would expect the advice to be applied around the call to Bar#name with the internal call to super (Foo#name) untouched, yielding:
In order to vote in support of an implementation of AOP in Ruby, I would require both versions of advice to be supported, with a reasonable way of distinguising the two.
I *do* want AOP in Ruby, and like the concept of "cuts" you have introduced here. Congratulations and many thanks for the work you've done so far. If you expand the RCR to include the functionality described above, I will gladly change my vote in favor of the RCR.
Jacob Fugal lukfugl@gmail.com
Just like matz, I like this part of the proposal:
Although I don't like the name. I think "wrap" would be more descriptive. If the implementation allows, it might be useful to be able to manipulate these wrapped modules after the fact: excluding the wrapped modules, redefining/removing methods in the wrapped modules, etc. But, if that becomes too complex or too much overhead, I think it would be fine to not give all that functionality. Starting with a baby step of just doing the above would be a great thing. This RCR to me is too big of a step and will face resistance.
I'm voting opposed because the "cut" concept and syntax doesn't seem very intuitive. I'd rather see the "preclude" above be a basis instead. Just with the above code you can almost immediately grasp the concept and it fits very well into what we already have.
Eric
Hi Jacob,
Intercepting a method in a subclass by defining "around" on a parent class is in a sense "dangerous" b/c a class would then be preventd from being subclassed properly. I'm not saying it can't be useful, but it should be considered carefully. This has come up in our discussions prior, and even with the stated danger set aside, it still befalls the 80/20 rule. It's not the most common use case. But fear not! An easy way achieve this behavior is with a singleton class, when you initialize a class have it define a singleton:
You can continue this by calling super in any subclass:
Considering what you propose and how if effects things, this is a much safer approach to accomplishing it. (And of course you can get even fancier and use something like Facet's preinclusion system ;)
Thanks for bringing this up.
T.
BTW, I do appreciate the thought and time that you guys put into this. Especially making a patch - not many go to that length. The patch also allows us to try out both "cut" and "preclude", right?
Eric
Eric, Just cuts at the moment. Using __cut__ keyword or Cut.new. I suspect Peter can whip up preclude pretty easily using proxy-cut. I'll ask him about it and see if he has time ( he's the c coder in the family ;) BTW there's a switch you can activate on compilation which allows one to cut modules too, just as one cuts classes, but it has a bad speed hit.
T.
Trans,
That does indeed work, and very well. I agree that this is a cleaner and safer approach to the "wrap the entire subtree" use case. Thanks for the feedback!
Changing my vote...
Jacob Fugal
I don't really understand why we need to do 'cut A < C' - if cuts are transparent we shouldn't need to assign them. 'cut C' should work just as well (the way I see it anyway, I might be wrong ofcourse; don't know much about AOP).
The cut name gives you a "handle". You can modify cuts dynamically just as you can classes and modules so giving the cut a name makes that possible. Eg.
Of course you can create annonymous cuts too: