Unlocking Traits With ‘var’ In Java 10

Local-variable type inference, or more succinctly var, turns out to be surprisingly versatile. It unlocks intersection types, allows using ad-hoc fields and methods of anonymous types, and, as we will see now, enables working with traits. In the end, I don’t think it’s worth the hassle, but your mileage may vary – maybe it helps you out of a bind one day.

Overview

To see the code snippets in action, check my Java 10 demo project.

What’s A Trait?

A trait ad-hoc combines features of several types into one

Some programming languages have a concept of traits, which allows creating a new type in the middle of a variable declaration that combines features of several types into one. Conceptionally, this could look as follows:

Looks nice, doesn’t it? If only the syntax wouldn’t be totally made up… But, as we will see in a minute, we can take a few steps into that direction with some trickery involving lambdas, default methods, and var. Before we come to that, though, I want to address a question you might have at this point.

Wait, I Thought These Were Intersection Types!?

If this reminds you of intersection types, you’re on to something. The ampersand in the declaration has the same semantics: The resulting intersection inherits from the joined types and it follows that variables of that intersection type have the methods of all joined types – in this case of Megacorp, IsSuccessful, and IsEvil.

The difference between intersection types and traits is that the former represent a requirement: A variable, parameter, or return value has to be of the intersection of the joined types. Traits, on the other hand, are about composing types: The variable created by the construction will be of the intersection of the joined types.

Naturally, intersection types and traits go well together:

Now, if only any of that were valid Java syntax… Let’s see how close we can get.

Creating The Skeleton For Traits

What the hell is going on here?

The basic recipe to creating traits has three ingredients:

  • prepare an interface that can be created with a lambda expression
  • cast that lambda to the intersection of the required types
  • assign the entire kerfuffle to a var-ed variable, so the type information don’t get trimmed to a single type

In the end, it will look like this:

I guess this leads us to the question: What the hell is going on here?

Let’s start with the initial type, Megacorp in this example: It needs to be an interface. To intersect it with other types, an extending interface like MegacorpDelegate needs to be created that default implements all methods by forwarding to a delegate:

The delegating interface must have a single abstract method, which returns the delegate

It is critical that the delegating interface’s only abstract method is one that returns the delegate. This enables you to create an instance of it from a given Megacorp with a lambda expression like () -> megacorp. Lambdas are crucial here because they are so-called poly expressions: They don’t have a defined type – instead it is inferred from the rest of the statement, in this case from the cast you place before them.

The delegating interface doesn’t add any domain functionality, it is only a technical requirement to make traits work in Java. As such, the delegating interface is conceptually close to the original one and I recommend placing it in the same package.

With that, some parts of the earlier declaration start to work:

Creating such a delegating interface gives you the skeleton that you can now flesh out with the actual traits by writing interfaces that extend the original interface and add new methods.

var traits

Creating Traits

After creating a delegating interface like MegacorpDelegate everything is set up for coding the actual traits. They add specific features to the original interface, here Megacorp, that are apparently not general enough to become part it. That’s why traits are more likely to end up close to where they are needed instead of close to the original interface.

Trait interfaces must not have abstract methods

The second critical requirement is that the trait interfaces must not have abstract methods! If they do, their intersection with the delegating interface could not be created with a lambda.

Here are some example for our megacorporations:

Because IsSuccessful and IsEvil have no abstract methods, their intersection with MegacorpDelegate can still be created with a lambda:

Et voilĂ , given a Megacorp instance you can decide on the spot which other functionality you want to compose with it. You can quickly and easily code that functionality up for your very specific context and start using it very naturally with plain method calls on the instance.

But the best thing is, neither the creators of Megacorp, MegacorpDelegate, or even IsSuccessful and IsEvil need to know what exactly you’re doing and neither do you need to create new types for any specific combination of traits. The latter is particularly important if you have many possible traits because the combinatorial explosion of creating a class for every combination would quickly overwhelm you.

The Dark Side

Ok, we’ve finally settled how to simulate traits in Java by using lambda expressions, default methods, intersection types, and local-variable type inference. This is a nice thought experiment and might even serve you as a stopgap one day, but it has severe disadvantages, which is why I don’t recommend to use it except in critical emergencies.

The first thing you might have noticed throughout this post is that simulating traits requires the combination of a decent number of non-trivial Java features, which always makes code less readable and thus less maintainable. Speaking of maintainability, variables of an intersection type only flourish within a method – as soon as they need to be passed to or from a method they require some generic trickery to be represented. This makes refactoring such code considerably more complicated.

There’s also some setup involved in creating the delegating interface and, even though that is unlikely to play a relevant role, the memory indirection it incurs reduces performance. Beyond that, traits need to be interfaces without abstract methods, which limits the functionality you can implement.

The final death knell is something else, though: Default methods can not implement Object methods, which has the consequence that the combined instances can never forward a call to equals or hashCode to the underlying delegate. This is bound to break code that puts such instances in collections and will result in strange and hard-to-debug misbehavior.

So as much fun as playing with traits is, I strongly recommend never to simulate them this way. Alternatives are to simply gather the required methods in utility classes or to create subclasses after all.

Reflection

These are the ingredients to ad-hoc compose traits with a given interface:

  • a delegating interface extending the given interface:
    • uses default methods to forward all calls to delegate
    • has single abstract method delegate(), so it can be created with () -> delegate
  • trait interfaces that have no abstract methods
  • declare variables as follows:

This looks like a great way to avoid deep inheritance trees and instead compose the exact behavior you need exactly where you need. Alas, it has several downsides:

  • cumbersome creation of the delegating interface
  • uses several non-trivial language features
  • use of intersection types makes refactoring harder
  • can not correctly implement equals/ hashCode

Share & Follow

You liked this post? Then share it with your friends and followers!
twittergoogle_plusredditlinkedin
And if you like what I'm writing about, why don't you follow me?
twittergoogle_plusrssmail

Other Posts