-
Object Services in Scala
Posted on June 21st, 2010 2 commentsUsing Scala’s implicits it’s possible to implement Object Services in a much more “user-friendly” way. Just to remind, the goal is to extend a class hierarchy with a method, polymorphically. E.g. we have:
trait Animal class Elephant extends Animal class Ant extends Animal
and we want to add a
paintmethod, which has a different implementation for an elephant, and for an ant (quite obviously :) ). In another words, we want to have a polymorphic extension method. One approach in Scala is to use pattern-matching. And in fact pattern-matching can be used to implement object services as described here (below is a method which uses reflection, but it can be easily swapped).Before we’ll go to the actual implementation, here’s how you can use the object services. We already have the objects, so it’s time to create the services:
trait PaintService[O <: Animal] extends OS[O] { def paint(c: Canvas): Unit } class ElephantPaintService(elephant: Elephant) extends PaintService[Elephant] { def paint(c: Canvas): Unit { ... } } class AntPaintService(ant: Ant) extends PaintService[Ant] { def paint(c: Canvas): Unit { ... } }Now we need a place, where we register the services. In the Weld implementation, this was done automatically at container startup time. Here we’ll need one object (which could be auto-generated by a compiler plugin):
object PaintServiceReg extends OSP[Animal, PaintService] { register[Elephant, ElephantPaintService] register[Ant, AntPaintService] }The type bound’s of the
registermethod will make sure that you can register only appropriate services for appropriate object types. Finally, we can use our services. If we want to use thePaintService, we’ll need to import the content of thePaintServiceRegobject. That way we have control, on what services are available when. Usage is quite simple and looks like a regular method invocation, as if the method was in the class hierarchy:import PaintServiceReg._ val animal1: Animal = new Elephant animal1.paint(c) // ElephantPaintService is called val animal2: Animal = new Ant animal2.paint(c) // AntPaintService is called
What’s left to show is the actual code for
OSandOSP:trait OS[O] trait OSP[O <: AnyRef, S[_ <: O] <: OS[_]] { implicit def oToS(obj: O): S[O] = { val bestService = findBestService(obj.getClass()) bestService.getConstructors()(0).newInstance(obj).asInstanceOf[S[O]] } protected def register[RO <: O, RS <: S[RO]](implicit manifestRO: Manifest[RO], manifestRS: Manifest[RS]) { serviceMap += manifestRO.erasure -> manifestRS.erasure } private var serviceMap: Map[Class[_], Class[_]] = Map() private def findBestService(objectCls: Class[_]): Class[_] = { serviceMap.foldLeft[(Class[_], Class[_])]((classOf[AnyRef], classOf[AnyRef])) ((curr, mapping) => { // Checking if the mapping is appropriate for the given object class // and more specific than the current one if (mapping._1.isAssignableFrom(objectCls) && curr._1.isAssignableFrom(mapping._1)) mapping else curr })._2 } }The implementation can be improved e.g. to always return the same object service reference for a given object (something like Eclipse Adapters – thanks for the link), or to provide alternative ways to instantiate the services – not necessarily with a one-arg constructor.
The biggest problem here is that completeness is not checked – that is, if there’s a service missing for a class in the hierarchy, there will be a run-time error. But I think this also can be checked with a compiler plugin.
-
Nonnull-check generator: a scala compiler plugin
Posted on October 15th, 2009 No commentsRecently I played a bit with compiler plugins for Scala.
My goal is to write a plugin which would generate checks in code for methods annotated with JSR305 annotations. As a first step, I wanted to handle the
@Nonnullannotation on parameters. Supposing you have the following code:def parameterMustBeNull(@Nonnull parameter : Object) = { parameter.toString(); }the compiler plugin should transform it to:
def parameterMustBeNull(@Nonnull parameter : Object) = { if (param == null) throw new IllegalArgumentException("Parameter 'parameter' should not be null.") parameter.toString(); }That way, the method contract expressed by annotations in the method header doesn’t have to be duplicated by explicit checks in the code later – they are generated automatically.
To achieve that, the plugin must first find the parameters that are annotated with
@Nonnull. Then, for each such parameter, it must generate a tree corresponding to theifpart. Finally, these generated chunks of code must be added to the beginning of the body of the method. All of this is quite easy to do thanks to the Scala compiler API and the AST that you can inspect and manipulate. But I think it’s best just to look at source code.In some more detail, as we want to transform the AST, the plugin must use the
Transformtrait. Then we have to implement the transform method, which looks if the tree passed is a method and if so, looks for the nonnull parameters:object AnnotationsCheckGenComponent extends PluginComponent with Transform { ... object AnnotationsCheckGenTransformer extends Transformer { override def transform(tree: Tree) = { tree match { // DefDef is a method definition case dd @ DefDef(_, _, _, vparamss, _, _) => { // here we add the nonnull checks } case t => super.transform(t) } }The
vparamsslist holds the list of parameters. To search for parameters which are annotated with a nonnull annotation, we can use the very handy Scala list manipulation methods:vparamss.flatten[ValDef].filter(param => { // Checking if the parameter contains a non-null annotation param.mods.annotations.exists(annotation => { // Checking if the annotation is a non-null annotation annotation.constr.find(tree => { tree match { case Ident(name) => "No[nt][Nn]ull".r.findFirstIn(name.toString).isDefined case _ => false } }).isDefined }) })As you can see above, any nonnull annotation will be detected, not only the ones defined by JSR305. And finally, for each parameter annotated, we must generate the if block containing the check. It’s almost like writing the code directly ;).
If( // if: param == ... Apply( Select( Ident(param.name), newTermName("$eq$eq")), List(Literal(Constant(null))) ), // then: throw ... Throw( Apply( Select( New(Ident(newTypeName("IllegalArgumentException"))), newTermName("<init>")), List(Literal(Constant("Parameter '%s' should not be null.".format(param.name)))))), // else EmptyTree)To use the plugin during compiling, all you need to do is have a jar (you can download a ready one here) that includes the compiled plugin class and a descriptor, and run the compilation with the
-Xpluginparameter, for example:scalac -cp $JSR305_JAR_PATH -Xplugin:annotations-check-gen.jar Example.scala
The only problem I had was in which phase the plugin should be executed. At first I tried executing it after
namer, and later, taking the advice I got on the forums (by the way, the forum members are really helpful), aftertyper. However, then I had to run the typer again on the generated code, and I didn’t yet figure out how to type the generatedifin a context of the method, so that the typer has access to the information about available values (here, the value being the parameter checked is used). So for now, instead, the plugin runs after theparserphase (so in fact as the first plugin), and then the result handled by the rest of the phases as if the code was there from the beginning. But if somebody has an idea, on how to run the typer properly, please let me know :).You can find the entire source code on github. For building and testing I used buildr. I must say that writing the build using that tool was a lot more pleasant than using Maven2, even considering the fact that I don’t know Ruby.
The plugin can be extended to generate checks for other annotations, for example
@MatchesPattern,@RegExetc. Another thing to do is to generate checks also for return values of methods (e.g. if a method is annotated with@Nonnull, this means that the return value shuoldn’t benull). This is a bit more tricky however, as there can be many exit points from the method. Going further, the generation can be extended to more advanced method contracts, expressed for example via typestate annotations.If you are also interested in writing compiler plugins for Scala you can find a good tutorial here, and a step-by-step guide describing another plugin here.
Adam

