Thursday, February 7, 2013

Using XTend

In the previous post I explained how I moved away from AspectJ towards XTend, so I want to share my experience in migrating. Let's start with a quick list of things I like:

Things that are cool

  • Polymorphic runtime dispatch
    • This was the main reason to switch to XTend
  • Template Strings
    • They can come in very handy when you want to create stuff like for example a HTML file
  • Extension Methods
    • You can "extend" any type and make things look more OOP like instead of the regular procedural code where you don't see the what the operation is actually performed on
  • The whole accessing getters as fields
    • I just have so many getters that it makes a lot of sense
  • The reduced noise by leaving ; away

A cool example:

def dispatch String toString(HDLConcat concat, SyntaxHighlighter highlight) 
  '''«FOR HDLExpression cat : concat.cats SEPARATOR highlight.operator("#")» «highlight.operator(cat.toString(highlight))» «ENDFOR»'''

Instead of:

public String HDLConcat.toString(SyntaxHighlighter highlight) {
 StringBuilder sb = new StringBuilder();
 String spacer = "";
 for (HDLExpression cat : getCats()) {
  sb.append(spacer).append(highlight.operator(cat.toString(highlight)));
  spacer = highlight.operator("#");
 }
 return sb.toString();
}

Things I am not passionate about:

  • val and var it's quite alright that you can leave the type away or that you can declare something as final, but I like my types visible and so I declare them anyway in most cases
  • The tooling. It is descent, but not yet something to brag about
  • The "everything is an expression" idea. Ternary operators are nice, no need to replace them with if then else constructs. So far I always wrote a return statement instead of relying on the "the last statement is the return value" thingy.
  • The all imports should be explicit (vs. the usage of wildcards)

Things that are not optimal:

  • In order to use nested classes, you will have to use the $ sign to refer to them. Knowing that this is the JVM type of referring to them is not a good excuse.

Things that are plain stupid:

  • As of now you can not allocate Arrays (because the [] brackets are used for closures). This however is already a planned change
  • There are no character literals. Because of stupid auto boxing I had to write a little helper method instead

So, instead of simply writing:

Character.toString((char) (i + 'I'))

I know had to write:

def String asIndex(Integer integer) {
    val int i='I'.charAt(0)
    return Character::toString((i + integer) as char);
}

Because this can be seen as an extension method, I was however able to write this:

i.asIndex
  • Working with arrays yields horrible performance because they are wrapped to a list
  • Working with enums, especially in switch cases is awkward to say the least

Here is a simple example:

switch (obj.presentation){
case HDLLiteral$HDLLiteralPresentation::STR:
    return null
case HDLLiteral$HDLLiteralPresentation::BOOL:
    return null
}

Where is your the type inference now? To be fair, you can create a static import for those enums, but switches are strange beasts in XTend. They are not real switch cases translated 1:1 to Java, instead they are cascaded if statements. Also fall throughs and empty cases are not supported.

switch (type: obj.type) {
case type==OR || type==XOR:{
    return Ranges::closed(ZERO, ONE.shiftLeft( leftRange.upperEndpoint.bitLength ).subtract( ONE ))
}
case AND: {
    return Ranges::closed(ZERO, leftRange.upperEndpoint.min( ONE.shiftLeft( rightRange.upperEndpoint.bitLength ).subtract( ONE )))
}
case type==LOGI_AND || type==LOGI_OR: {
    return Ranges::closed(ZERO, ONE)
}

The resulting Java code for the last example is:

boolean _matched = false;
if (!_matched) {
 boolean _or = false;
 boolean _equals = Objects.equal(type, HDLBitOpType.OR);
 if (_equals) {
   _or = true;
 } else {
   boolean _equals_1 = Objects.equal(type, HDLBitOpType.XOR);
   _or = (_equals || _equals_1);
 }
 if (_or) {
   _matched=true;
   BigInteger _upperEndpoint = leftRange.upperEndpoint();
   int _bitLength = _upperEndpoint.bitLength();
   BigInteger _shiftLeft = BigInteger.ONE.shiftLeft(_bitLength);
   BigInteger _subtract = _shiftLeft.subtract(BigInteger.ONE);
   return Ranges.<BigInteger>closed(BigInteger.ZERO, _subtract);
 }
}
if (!_matched) {
 if (Objects.equal(type,HDLBitOpType.AND)) {
   _matched=true;
   BigInteger _upperEndpoint_1 = leftRange.upperEndpoint();
   BigInteger _upperEndpoint_2 = rightRange.upperEndpoint();
   int _bitLength_1 = _upperEndpoint_2.bitLength();
   BigInteger _shiftLeft_1 = BigInteger.ONE.shiftLeft(_bitLength_1);
   BigInteger _subtract_1 = _shiftLeft_1.subtract(BigInteger.ONE);
   BigInteger _min = _upperEndpoint_1.min(_subtract_1);
   return Ranges.<BigInteger>closed(BigInteger.ZERO, _min);
 }
}
if (!_matched) {
 boolean _or_1 = false;
 boolean _equals_2 = Objects.equal(type, HDLBitOpType.LOGI_AND);
 if (_equals_2) {
   _or_1 = true;
 } else {
   boolean _equals_3 = Objects.equal(type, HDLBitOpType.LOGI_OR);
   _or_1 = (_equals_2 || _equals_3);
 }
 if (_or_1) {
   _matched=true;
   return Ranges.<BigInteger>closed(BigInteger.ZERO, BigInteger.ONE);
 }
}

My summary

XTend is a nice language, but in some places strange decisions have been made. Regarding the language, as well as regarding the realization of the generated code. Here a last example that is just plain horrible:

def static listTest() {
    val list=new ArrayList<String>
    for (i:0..list.size-1)
        System::out.println("Item:"+i+" is "+list.get(i))
}

If the list is empty (as it is because I didn't add anything) it will crash because the index 0 is out of range. And it is not like you could use the plain java style for loop. You have to create a range...

No comments:

Post a Comment