Automatic generation of delegate methods with Macro Annotations

25 Flares 25 Flares ×

Macro Annotations are a new type of macros, which are one of the candidates for inclusion (see also comment by Eugene below) in the upcoming Scala 2.11 release. However, thanks to the recently released Macro Paradise Scala 2.10 compiler plugin, with an extra option in the compiler/SBT settings, you can use them today, while still using a stable Scala version at runtime.

One of the Macro Annotations use-cases mentioned in the manual is compile-time AOP. I decided to try implementing something similar, but a bit simpler for a start: automatic generation of delegate methods (decorator pattern/proxy pattern). In fact, some years ago there was a similar effort using a compiler plugin (autoproxy plugin). As an additional motivation, Łukasz recently asked on our technical room if Scala has this exact functionality – instead of saying “No”, I should have said “Not yet” ;).

The results of the POC are available on GitHub, in the scala-macro-aop repository: https://github.com/adamw/scala-macro-aop. If you have SBT, you can play with the implementation just by invoking run from the SBT console.

How does it work? Let’s say we have an interface Foo with three methods (with very original names: method1, method2 and method3), each taking some parameters. We have a default implementation:

1
2
3
4
5
6
7
8
9
10
11
trait Foo {
   def method1(param1: String): Int
   def method2(p1: Int, p2: Long): Float
   def method3(): String
}
 
class FooImpl extends Foo {
   def method1(param1: String) = param1.length
   def method2(p1: Int, p2: Long) = p1 + p2
   def method3() = "Hello World!"
}

Now we would like to create a wrapper for a Foo instance, which would delegate all method calls to the given instance, unless the method is defined in the wrapper.

The traditional solution is to create a delegate for each method by hand, e.g.:

1
2
3
4
5
class FooWrapper(wrapped: Foo) extends Foo {
   def method1(param1: String) = wrapped.method1(param1)
   def method2(p1: Int, p2: Long) = wrapped.method2(p1, p2)
   def method3() = wrapped.method3()
}

But that’s a lot of work. Using the @delegate macro, the delegate methods will now be automatically generated at compile time! That is, the wrapper now becomes:

1
2
3
4
class FooWrapper(@delegate wrapped: Foo) extends Foo {
   // method1, method2 and method3 are generated at compile time
   // and delegate to the annotated parameter
}

What if we want to implement some methods? The macro will generate only the missing ones:

1
2
3
4
class FooWrapper(@delegate wrapped: Foo) extends Foo {
   def method2(p1: Int, p2: Long) = p1 - p1
   // only method1 and method3 are generated
}

As the implementation is just a POC, it will only work in simple cases, that is for methods with a single parameter list, without type parameters and when the method is not overloaded. Plus the code of the macro is, let’s say, “not yet polished” ;).

As mentioned before, the code is on GitHub: https://github.com/adamw/scala-macro-aop, available under the Apache2 license.

Adam

UPDATE 9/9/2013: clarifying Macro Annotations availability in 2.11

  • http://xeno.by Eugene Burmako

    Thanks for doing this cool experiment!

    I would like to notice, however, that it’s not yet decided whether macro annotations end up in 2.11. Actually since we haven’t yet agreed upon the details of this feature, it’s quite likely that 2.11 won’t have it.

  • http://www.warski.org/ Adam Warski

    Ah, thanks for pointing that out :) I updated the blog. Though it would be a pity if Macro Annotations weren’t there for 2.11. Still, luckily there’s paradise ;)

  • Marcin Burczak

    To jest wypaczanie wzorców projektowych. Po to robi się dekoratory albo proxy aby zmieniać zachowanie klasy nie tylko delegować wywołania do oryginalnej implementacji. Ktoś powie, że metod może być dużo a chcemy zmienić zachowanie tylko niektórych. Czy w takiej sytuacji nasza klasa na pewno ma jedną odpowiedzialność? Po co w ogóle bawić się we wzorce obiektowe jak mamy język funkcyjny? Dla mnie to jest przkrywanie rzeźby inną rzeźbą bo się nie chce robić refaktoringu. Jeśli po o mają służyc makra to jestem na nie.

    PS Dlaczego użyłeś takiej nazwy: FooImpl? Załamuję się jak widze takie coś i to na dzone.

  • http://www.warski.org/ Adam Warski

    Czemu piszesz po polsku, jak całość jest po angielsku?

    W sytuacji gdzie kontrolujemy całość źródeł – możliwe że masz racje. Dobrze byłoby pewnie takie coś zrefaktorować, podzielić itd. Czasami jednak trzeba się zintegrować z jakąś zewnętrzną biblioteką, opakować jakąś klasę nad którą nie mamy kontroli, albo po prostu godzimy się na wprowadzenie takiego “hacka” bo jest to duże tańsze (wliczając w to koszt brzydkiego kodu) niż refaktoring.

    Język mamy funkcyjno-obiektowy, a nie funkcyjny (Scala). Można nadal używać wzorców obiektowych, jeżeli ma to sens? Używanie wszędzie programowania funkcyjnego “na siłę” może mieć chyba równie złe skutki co używanie wszędzie programowania obiektowego.

    Ad FooImpl – ja się załamuje jak ktoś się czepia nieistotnych szczegółów. A tak naprawdę nazwałem tak żeby zrobić Ci na złość ;)

  • Marcin Burczak

    Sorry za polski ale nie chcę kaleczyć angielskiego (czytam ze zrozumieniem ale nie potrafię się wyrazić zbyt dobrze).

    Jeżeli w tak, stosunkowo młodym języku jak Scala trzeba już robić hacki, to coś tu śmierdzi. Wiem, że jest to język hybrydowy ale dla mnie programowanie funkcyjne wyprzedza o wiele obiektowe. Wzorce projektowe powstały po części z braków w językach obiektowych, braków m.in. funkcji wyższego rzędu. Po co mamy posiłkować się takimi “zaślepkami” jak możemy to zrobić funkcyjnie? Czy będzie to “na siłę”? Twórcy języka kładą nacisk na programowanie funkcyjne i powinniśmy do tego dążyć. Osobiście uważam, że to może się skończyć jak z Javą i obiektowością: większość myśli, że jak pisze w Javie to z automatu pisze obiektowo, a tak naprawdę programują proceduralnie. Jeżeli w Scali można pisać tak jak w Javie, to też będzie powstawał kod pseudo-obiektowy i będzie to bardzo smutne. Programowanie obiektowe nie jest złe ale wszystkie wzorce i dobre praktyki pchają nas do programowania funkcyjnego.

    FooImpl – dla Ciebie to “nieistotny szczegół” a dla mnie to jeden z największych błędów jakie można popełnić programując obiektowo.

25 Flares Twitter 18 Facebook 0 Google+ 6 LinkedIn 1 Email -- 25 Flares ×