A Scala-style "with" Construct for Ruby 108
Scala has a “mixin” construct called traits, which are roughly analogous to Ruby modules. They allow you to create reusable, modular bits of state and behavior and use them to compose classes and other traits or modules.
The syntax for using Scala traits is quite elegant. It’s straightforward to implement the same syntax in Ruby and doing so has a few useful advantages.
For example, here is a Scala example that uses a trait to trace calls to a Worker.work
method.
// run with "scala example.scala"
class Worker {
def work() = "work"
}
trait WorkerTracer extends Worker {
override def work() = "Before, " + super.work() + ", After"
}
val worker = new Worker with WorkerTracer
println(worker.work()) // => Before, work, After
Note that WorkerTracer
extends Worker
so it can override the work
method. Since Scala is statically typed, you can’t just define an override
method and call super
unless the compiler knows there really is a “super” method!
Here’s a Ruby equivalent.
# run with "ruby example.rb"
module WorkerTracer
def work; "Before, #{super}, After"; end
end
class Worker
def work; "work"; end
end
class TracedWorker < Worker
include WorkerTracer
end
worker = TracedWorker.new
puts worker.work # => Before, work, After
Note that we have to create a subclass, which isn’t required for the Scala case (but can be done when desired).
If you know that you will always want to trace calls to work
in the Ruby case, you might be tempted to dispense with the subclass and just add include WorkerTracer
in Worker
. Unfortunately, this won’t work. Due to the way that Ruby resolves methods, the version of work
in the module will not be found before the version defined in Worker
itself. Hence the subclass seems to be the only option.
However, we can work around this using metaprogramming. We can use WorkerTracer#append_features(...)
. What goes in the argument list? If we pass Worker
, then all instances of Worker
will be effected, but actually we’ll still have the problem with the method resolution rules.
If we just want to affect one object and work around the method resolution roles, then we need to pass the singleton class (or eigenclass or metaclass ...) for the object, which you can get with the following expression.
metaclass = class << worker; self; end
So, to encapsulate all this and to get back to the original goal of implementing with
-style semantics, here is an implementation that adds a with
method to Object
, wrapped in an rspec example.
# run with "spec ruby_with_spec.rb"
require 'rubygems'
require 'spec'
# Warning, monkeypatching Object, especially with a name
# that might be commonly used is fraught with peril!!
class Object
def with *modules
metaclass = class << self; self; end
modules.flatten.each do |m|
m.send :append_features, metaclass
end
self
end
end
module WorkerTracer
def work; "Before, #{super}, After"; end
end
module WorkerTracer1
def work; "Before1, #{super}, After1"; end
end
class Worker
def work; "work"; end
end
describe "Object#with" do
it "should make no changes to an object if no modules are specified" do
worker = Worker.new.with
worker.work.should == "work"
end
it "should override any methods with a module's methods of the same name" do
worker = Worker.new.with WorkerTracer
worker.work.should == "Before, work, After"
end
it "should stack overrides for multiple modules" do
worker = Worker.new.with(WorkerTracer).with(WorkerTracer1)
worker.work.should == "Before1, Before, work, After, After1"
end
it "should stack overrides for a list of modules" do
worker = Worker.new.with WorkerTracer, WorkerTracer1
worker.work.should == "Before1, Before, work, After, After1"
end
it "should stack overrides for an array of modules" do
worker = Worker.new.with [WorkerTracer, WorkerTracer1]
worker.work.should == "Before1, Before, work, After, After1"
end
end
You should carefully consider the warning about monkeypatching Object
! Also, note that Module.append_features
is actually private, so I had to use m.send :append_features, ...
instead.
The syntax is reasonably intuitive and it eliminates the need for an explicit subclass. You can pass a single module, or a list or array of them. Because with
returns the object, you can also chain with
calls.
A final note; many developers steer clear of metaprogramming and reflection features in their languages, out of fear. While prudence is definitely wise, the power of these tools can dramatically accelerate your productivity. Metaprogramming is just programming. Every developer should master it.
Traits vs. Aspects in Scala 91
Scala traits provide a mixin composition mechanism that has been missing in Java. Roughly speaking, you can think of traits as analogous to Java interfaces, but with implementations.
Aspects, e.g., those written in AspectJ, are another mechanism for mixin composition in Java. How do aspects and traits compare?
Let’s look at an example trait first, then re-implement the same behavior using an AspectJ aspect, and finally compare the two approaches.
Observing with Traits
In a previous post on Scala, I gave an example of the Observer Pattern implemented using a trait. Chris Shorrock and James Iry provided improved versions in the comments. I’ll use James’ example here.
To keep things as simple as possible, let’s observe a simple Counter
, which increments an internal count variable by the number input to an add
method.
package example
class Counter {
var count = 0
def add(i: Int) = count += i
}
The count
field is actually public, but I will only write to it through add
.
Here is James’ Subject trait that implements the Observer Pattern.
package example
trait Subject {
type Observer = { def receiveUpdate(subject:Any) }
private var observers = List[Observer]()
def addObserver(observer:Observer) = observers ::= observer
def notifyObservers = observers foreach (_.receiveUpdate(this))
}
Effectively, this says that we can use any object as an Observer
as long as it matches the structural type { def receiveUpdate(subject:Any) }
. Think of structural types as anonymous interfaces. Here, a valid observer is one that has a receiveUpdate
method taking an argument of Any
type.
The rest of the trait manages a list of observers and defines a notifyObservers
method. The expression observers ::= observer
uses the List
::
(“cons”) operator to prepend an item to the list. (Note, I am using the default immutable List
, so a new copy is created everytime.)
The notifyObservers
method iterates through the observers, calling receiveUpdate
on each one. The _
that gets replaced with each observer during the iteration.
Finally, here is a specs file that exercises the code.
package example
import org.specs._
object CounterObserverSpec extends Specification {
"A Counter Observer" should {
"observe counter increments" in {
class CounterObserver {
var updates = 0
def receiveUpdate(subject:Any) = updates += 1
}
class WatchedCounter extends Counter with Subject {
override def add(i: Int) = {
super.add(i)
notifyObservers
}
}
var watchedCounter = new WatchedCounter
var counterObserver = new CounterObserver
watchedCounter.addObserver(counterObserver)
for (i <- 1 to 3) watchedCounter.add(i)
counterObserver.updates must_== 3
watchedCounter.count must_== 6
}
}
}
The specs library is a BDD tool inspired by rspec in Rubyland.
I won’t discuss it all the specs-specific details here, but hopefully you’ll get the general idea of what it’s doing.
Inside the "observe counter increments" in {...}
, I start by declaring two classes, CounterObserver
and WatchedCounter
. CounterObserver
satisfies our required structural type, i.e., it provides a receiveUpdate
method.
WatchedCounter
subclasses Counter
and mixes in the Subject
trait. It overrides the add
method, where it calls Counter
’s add
first, then notifies the observers. No parentheses are used in the invocation of notifyObservers
because the method was not defined to take any!
Next, I create an instance of each class, add the observer to the WatchedCounter
, and make 3 calls to watchedCounter.add
.
Finally, I use the “actual must_== expected
” idiom to test the results. The observer should have seen 3 updates, while the counter should have a total of 6.
The following simple bash shell script will build and run the code.
SCALA_HOME=...
SCALA_SPECS_HOME=...
CP=$SCALA_HOME/lib/scala-library.jar:$SCALA_SPECS_HOME/specs-1.3.1.jar:bin
rm -rf bin
mkdir -p bin
scalac -d bin -cp $CP src/example/*.scala
scala -cp $CP example/CounterObserverSpec
Note that I put all the sources in a src/example
directory. Also, I’m using v1.3.1 of specs, as well as v2.7.1 of Scala. You should get the following output.
Specification "CounterObserverSpec"
A Counter Observer should
+ observe counter increments
Total for specification "CounterObserverSpec":
Finished in 0 second, 60 ms
1 example, 2 assertions, 0 failure, 0 error
Observing with Aspects
Because Scala compiles to Java byte code, I can use AspectJ to advice Scala code! For this to work, you have to be aware of how Scala represents its concepts in byte code. For example, object declarations, e.g., object Foo {...}
become static final classes. Also, method names like +
become $plus
in byte code.
However, most Scala type, method, and variable names can be used as is in AspectJ. This is true for my example.
Here is an aspect that observes calls to Counter.add
.
package example
public aspect CounterObserver {
after(Object counter, int value):
call(void *.add(int)) && target(counter) && args(value) {
RecordedObservations.record("adding "+value);
}
}
You can read this aspect as follows, after calling Counter.add
(and keeping track of the Counter object that was called, and the value passed to the method), call the static method record
on the RecordedObservations
.
I’m using a separate Scala object RecordedObservations
package example
object RecordedObservations {
private var messages = List[String]()
def record(message: String):Unit = messages ::= message
def count() = messages.length
def reset():Unit = messages = Nil
}
Recall that this is effectively a static final Java class. I need this separate object, rather than keeping information in the aspect itself, because of the simple-minded way I’m building the code. ;) However, it’s generally a good idea with aspects to delegate most of the work to Java or Scala code anyway.
Now, the “spec” file is:
package example
import org.specs._
object CounterObserverSpec extends Specification {
"A Counter Observer" should {
"observe counter increments" in {
RecordedObservations.reset()
var counter = new Counter
for (i <- 1 to 3) counter.add(i)
RecordedObservations.count() must_== 3
counter.count must_== 6
}
}
}
This time, I don’t need two more classes for the adding a mixin trait or defining an observer. Also, I call RecordedObservations.count
to ensure it was called 3 times.
The build script is also slightly different to add the AspectJ compilation.
SCALA_HOME=...
SCALA_SPECS_HOME=...
ASPECTJ_HOME=...
CP=$SCALA_HOME/lib/scala-library.jar:$SCALA_SPECS_HOME/specs-1.3.1.jar:$ASPECTJ_HOME/lib/aspectjrt.jar:bin
rm -rf bin app.jar
mkdir -p bin
scalac -d bin -cp $CP src/example/*.scala
ajc -1.5 -outjar app.jar -cp $CP -inpath bin src/example/CounterObserver.aj
aj -cp $ASPECTJ_HOME/lib/aspectjweaver.jar:app.jar:$CP example.CounterObserverSpec
The ajc
command not only compiles the aspect, but it “weaves” into the compiled Scala classes in the bin
directory. Actually, it only affects the Counter
class. Then it writes all the woven and unmodified class files to app.jar
, which is used to execute the test. Note that for production use, you might prefer load-time weaving.
The output is the same as before (except for the milliseconds), so I won’t show it here.
Comparing Traits with Aspects
So far, both approaches are equally viable. The traits approach obviously doesn’t require a separate language and corresponding tool set.
However, traits have one important limitation with respect to aspects. Aspects let you define pointcuts that are queries over all possible points where new behavior or modifications might be desired. These points are called join points in aspect terminology. The aspect I showed above has a simple pointcut that selects one join point, calls to the Counter.add
method.
However, what if I wanted to observe all state changes in all classes in a package? Defining traits for each case would be tedious and error prone, since it would be easy to overlook some cases. With an aspect framework like AspectJ, I can implement observation at all the points I care about in a modular way.
Aspect frameworks support this by providing wildcard mechanisms. I won’t go into the details here, but the *
in the previous aspect is an example, matching any type. Also, one of the most powerful techniques for writing robust aspects is to use pointcuts that reference only annotations, a form of abstraction. As a final example, if I add an annotation Adder
to Counter.add
,
package example
class Counter {
var count = 0
@Adder def add(i: Int) = count += i
}
Then I can rewrite the aspect as follows.
package example
public aspect CounterObserver {
after(Object counter, int value):
call(@Adder void *.*(int)) && target(counter) && args(value) {
RecordedObservations.record("adding "+value);
}
}
Now, there are no type and method names in the pointcut. Any instance method on any visible type that takes one int
(or Scala Int
) argument and is annotated with Adder
will get matched.
Note: Scala requires that you create any custom annotations as normal Java annotations. Also, if you intend to use them with Aspects, use runtime retention policy, which will be necessary if you use load-time weaving.
Conclusion
If you need to mix in behavior in a specific, relatively-localized set of classes, Scala traits are probably all you need and you don’t need another language. If you need more “pervasive” modifications (e.g., tracing, policy enforcement, security), consider using aspects.
Acknowledgements
Thanks to Ramnivas Laddad, whose forthcoming 2nd Edition of AspectJ in Action got me thinking about this topic.