The thought of wanting to support typed channels between our dear untyped actors has been on my mind since at least ScalaDays 2012, sparked anew by Simon Peyton-Jones’ talk about Haskell in the Cloud. But at the time the only means available was the Scala type system which—while certainly powerful enough—made this task daunting, to say the least. Scala 2.10’s macros change the game completely, as I’ll demonstrate in this post.
The simple requirement for the end product is that the compiler shall flag type errors whenever a message is sent to an actor which is statically known not to handle that type of message. Without going too much into the details (expect a future post on this), there are two basic complications: the dynamic nature of the “sender” reference within the receiving actor and the fact that Scala does not support type unions. The latter means that instead of assigning a single message type to a channel, composability of actor systems demands that it must be possible to describe a channel which takes A or B, but without using the potentially meaningless least-upper-bound (which will in many cases be AnyRef). Therefore the venture will involve type lists. The first complication then demands that the reply channel needs to have a type which depends on which element of the set of input types is chosen during a message send; this translates into the need of formulating type maps.
As someone else would put it: “the type lambdas would look ghastly” without macros.
Getting Geared Up
First I added a new sub-project to the Akka build definition:
This is basically copied from the macros documentation. Now let’s define the basic types we will work with:
This also includes the basic scaffolding for about any macro you are going to write. Note the Context argument in its own argument list, upon which all relevant types depend, and which is additionally specialized to work only on ChannelRefs (which makes it possible to use c.prefix in a properly typed way).
Thrusting the Hands into the Muck
Now let’s walk right through the implementation of the tell macro. The first nested method we encounter is used to obtain the sender channel for the tell operation which is our current focus.
The first useful thing to note here is how easily accessible implicit resolution is. From the implicitly found ChannelRef I grab the type parameter (which is a ChannelList) and return that together with the found implicit itself (for later error reporting) and a tree which represents the corresponding ActorRef. The latter is obtained by simply invoking the actorRef method on the ChannelRef tree, but since this is looked up “dynamically” the compiler cannot know at this point that that method returns an ActorRef, necessitating the explicit type ascription to the returned expression. There is a very similar method for obtaining an implicit ActorRef, should no ChannelRef be found.
Error reporting is very important with macros, otherwise things which were supposed to magically work fail in magical ways, which leads to completely unintelligible error messages to everyone but the macro author. One limitation to be aware of is that it is currently not possible to report more than one error per source location (and errors suppress warnings).
Before showing the rest of the tell macro itself, we will need to look at the method which performs the lookup in the aforementioned type map:
There are a few things to note here:
- since all involved types are path-dependent, we need to pass around the singleton type they are bound to, in this case a scala.reflect.api.Universe
- making the method tail recursive (which it was initially) is not a good idea, because there will be weird type errors like “found: u.Type, required: u.Type”, see SI-6900; the nested tail-recursive method has no such issues
- it is possible to obtain TypeTags for types defined in the Scala library and below using this Universe, but doing so for user-defined types fails without using an Importer (see SI-6937, or the work-around)
- finally, lean back and enjoy the simplicity of this traditional list traversal, contrasting it with the gruesome details hidden beneath the surface of the extraordinary shapeless library which make such things work purely within the type system.
After letting that sink in for a while we are prepared to look at the heart of the macro:
Back on our main story-line, this algorithm verifies that the ping-pong of messages potentially following this initial send—everyone replying over and over—will not at some point result in a message type which is not handled by the receiving party. Again, this is bog-standard Scala code operating on normal collections, interspersed with macro error reporting. This verification step will set the error flag on the generated tree in case of problems, which then leads to the corresponding message being shown e.g. in Eclipse.
So, this wraps it up: splice the target ChannelRef, the message and the sender into a tree which is just your plain old ActorRef.tell() invocation. All the complicated types just vanished in a puff of smoke, also curtesy to ChannelRef being a value class.
Yes, Nice, but How do I Test That?
Well, positive tests are quite easy:
But they are not what we are after. The point of the macro is to disallow certain code, and do so at compile time. Therefore we have to compile the code during the test. This needs some extra leg-work up-front in the build definition:
Putting the tests into a different project is necessary to make compilation work in Eclipse, since that compiles all source files within a project together, i.e. it does not respect the staging which is required to make macros available for dependent sources.
Now with these two handy little helpers we can write negative tests like so:
Where does that lead Akka?
Macros enable very nice features which were not thinkable before, and we will certainly make good use of them. But the code shown above is not literally what will be contained within the next release; the API presented for typed channels will change soon, based on the lessons learned so far. Stay tuned for more: if you are interested in this stuff, you are welcome to vote for my talk at NEScala!