Tuesday, June 18, 2013

Xtend's Extension Providers

Xtend supports extension methods very similar to how they are supported by C#. The basic idea is to make static methods available as instance method on the first argument's type. Example :

import static extension java.util.Collections.*

...

// we can use static methods from Collections like this 
val maxValue = myCollection.max // calls Collections.max(myCollection)
val minValue = myCollection.min // calls Collections.min(myCollection)
...
 

This makes code much more readable as you don't have to read inside out, but can read from left to right. Also the discoverabilty of available features via content assist works much better.

But of course using static methods all over the place is problematic, as you bind your code to the implementation which makes it hard to test and to reconfigure for different situations (e.g. use a different database system).

Enter Extension Providers!

In Xtend you can put the extension keyword to a local field or a parameter. This will make its instance methods available on the first parameter's type.

I'll explain that with an example using JPA and Java EE.

 // Java code
@PersistenceContext
EntityManager em

public LineItem createLineItem(Order order, Product product, int quantity) {
    LineItem li = new LineItem(order, product, quantity);
    order.getLineItems().add(li);
    em.persist(li);
    return li;
}

public void removeOrder(Integer orderId) {
    Order order = em.find(Order.class, orderId);
    em.remove(order);
}
 

If you add the keyword extension to the field declaration, the instance methods of the PersistenceManager get projected onto the entities. So you no longer have to write em.persist(li) but can just write li.persist:


@PersistenceContext extension EntityManager

def createLineItem(Order order, Product product, int quantity) {
    val li = new LineItem(order, product, quantity)
    order.lineItems += li
    li.persist
    return li
}

def removeOrder(Integer orderId) {
    val order = Order.find(orderId)
    order.remove
}
 

Extension provider allow for adding layer specific functionality to any classes in a non-invasive way. And you don't have to use static methods for that.

Better APIs with Extension Providers

When designing an API, you can make it very easy for the clients to have the right extension providers on the scope.

One possibility is to provide an abstract base class which contains visibly extension fields:


abstract class AbstractDao {
    @PersistenceContext
    protected extension EntityManager em; 
}

class Concrete extends AbstractDao {
 // use extension methods from EntityManager
}
 

If you don't like inheritance that much, another approach is to mark a parameter of an abstract method with the extension keyword.

interface JPACallBack {
    def void doStuff(extension EntityManager em)
}
 

When implementing that method, the IDE will automatically add the extension keword for you. The active annotations API uses that idiom.

Note that in case you define the abstract class or the interface in Java, you can add the @Extension annotation instead of the keyword.

IDE Support

Sometimes the reader might be a bit unsure where a certain member has been declared. Of course the hover as well as navigation always shows the correct declaration. In addition the semantic coloring highlights extension methods ...

...and you can even inspect the desugared version of an expression in the hover :

6 comments:

  1. Very cool! Can I use this with multiple arguments as well? I don't seem to be able to.

    EG:

    Works:
    var String email = studyid.findEmailByStudyid


    Doesn't work:
    var ParticipantImmutable participant = (studyid, email).findParticipantByStudyidAndEmail

    ReplyDelete
    Replies
    1. Sure, but you'll have to pass the second (and any additional) arguments like so:

      var participant = studyid.findParticipantByStudyidAndEmail(email)

      Delete
    2. Works, thanks! I couldn't figure this out from reading the docs. This might be a good addition.

      Delete
  2. Hi Sven,

    Is there a way to avoid xtend from interpreting a method as an extension ?
    Or maybe is that I'm just facing a bug.

    While trying to use xtend on top of wicket, I have this issue.

    (almost) every wicket component can have children. You add then through this vararg method:

    public MarkupContainer add(final Component... childs)

    Declared in MarkupContainer.

    Then I have a Page (MarkupContainer subclass), where I want to add a component like this:

    this.add(form)

    But then xtend generates the following Java line:

    this.add(this, form)

    It's like if it uses the target object as a first argument call.
    Of course this runs into an error because you cannot add a component to itself.

    Can I force xtend not to do this ?

    Thanks !

    By the way, I'm a professor at several Public National Universities in Argentina, where we teach DSL concepts using xtext in some subjects. But we are also starting to teach object programming with xtend. It's really nice. Good job ! :)

    ReplyDelete
    Replies
    1. Hi Javier, this looks like a bug.
      Sugared feature calls (including extension calls) have lower precedence. So it should bind to the obvious.

      With which version do you experience the described behaviour?
      Could you file a bugzilla [1], with some steps to reproduce it?

      Thanks!

      [1] - https://bugs.eclipse.org/bugs/enter_bug.cgi?product=Xtend&component=Core

      Delete