In this blog I'm going to compare the core of the three principle 'closure' proposals. This is particularly apt following the recent surge in interest after Josh Bloch's Javapolis talk.
Comparing 'closure' proposals
The three principle closure proposals are:
- BGGA - full closures for Java
- CICE - simplified inner classes (with the related ARM proposal)
- FCM - first class methods (with the related JCA proposal)
It is easy to get confused when evaluating these competing proposals. So much new syntax. So many new ideas. For this blog I'll summarise, and then deep dive into one area.
The first key point that I have been making recently is that closures is not 'all or nothing' - there are parts to the proposals that can be implemented separately. This table summarises the 5 basic parts in the proposals (I've split the last one into two rows):
BGGA | CICE+ARM | FCM+JCA | |
---|---|---|---|
Literals for reflection | - | - | FCM member literals |
References to methods | - | - | FCM method references |
One method callback classes | BGGA closures | CICE | FCM inner methods |
Function types | BGGA function types | - | FCM method types |
Library based Control Structure | BGGA control invocation | - | JCA control abstraction |
Language based Control Structure | - | ARM | - |
Of course a table like this doesn't begin to do justice to any of the three proposals. Or to express the possible combinations (for example, BGGA could add support for the first two rows easily). So, instead of talking generally, lets examine a key use case where all 'closure' proposals work.
One method callbacks
This issue in Java refers to the complexity and verbosity of writing an anonymous inner class with one method. These are typically used for callbacks - where the application code passes a piece of logic to a framework for it to be executed later in time. The classic example is the ActionListener:
public void init() { button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ev) { // handle the event } }); }
This registers a callback that will be called by the framework whenever the button is pressed. We notice that the code in the listener will typically be called long after the init() method has completed. While this is an example from swing, the same design appears all over Java code.
Some of the issues with this code are immediately obvious. Some are not.
- The declaration of the listener is very verbose. It takes a lot of code to define something that is relatively simple, and importantly, that makes it harder to read.
- Secondly, information is duplicated that could be inferred by the compiler. Its not very DRY.
- The scope of the code inside actionPerformed() is different to that of the init() method.
The
this
keyword has a different meaning. - Any variables and methods from the interface take preference to those available in init(). For example, toString(), with any number of parameters, will always refer to the inner class, not the class that init() is declared in. In other words, the illusion that the code in actionPerformed() has full access to the code visible in init() is just that - just an illusion.
The three closure proposals differ in how they tackle this problem area. BGGA introduces full closures. These eliminate all the problems above, but introduce new issues with the meaning of return. CICE introduces a shorthand way of creating an inner class. This solves the first issue, and some of the second, but does not solve issue 3 or 4. FCM introduces inner methods. These eliminate all the problems above, and add no new surprises.
// BGGA public void init() { button.addActionListener({ActionEvent ev => // handle the event }); } // CICE public void init() { button.addActionListener(ActionListener(ActionEvent ev) { // handle the event }); } // FCM public void init() { button.addActionListener(#(ActionEvent ev) { // handle the event }); }
In this debate, it is easy to get carried away by syntax, and say that one of these might look 'prettier', 'more Java' or 'ugly'. Unfortunately, the syntax isn't that important - its the semantics that matter.
BGGA and FCM have semantics where this
within the callback refers to the surrounding class,
exactly as per code written directly in the init() method.
This is emphasised by removing all signs of the inner class.
The scope confusion of inner classes (issues 3 and 4 above) disappears (as this
is 'lexically scoped').
CICE has semantics where this
within the callback still refers to the inner class.
Only you can't even see the inner class now, its really well hidden.
This simply perpetuates the scope confusion of inner classes (issues 3 and 4 above) and gains us little other than less typing.
In fact, as the inner class is more hidden, this might actually be more confusing than today.
BGGA also has semantics where a return
within the callback will return from init().
This generally isn't what you want, as init() will be long gone when the button press occurs.
(It should be noted that this feature is a major gain to the overall power of Java, but comes with a high complexity factor)
FCM and CICE have semantics where return
within the callback returns from the callback,
as in an inner class. This enables easy conversion of code from today's inner classes.
(For FCM, the return
is considered to be back to the # symbol, a simple rule to learn)
And here is my personal summary of this discussion:
// BGGA public void init() { button.addActionListener({ActionEvent ev => // return will return to init() - BAD for many (most) use cases // this means the same as within init() - GOOD and simple }); } // CICE public void init() { button.addActionListener(ActionListener(ActionEvent ev) { // return will return to caller of the callback - GOOD for most use cases // this refers to the inner class of ActionListener - BAD and confusing }); } // FCM public void init() { button.addActionListener(#(ActionEvent ev) { // return will return to caller of the callback - GOOD for most use cases // this means the same as within init() - GOOD and simple }); }
Summary
This blog has evaluated just one part of the closures proposals. And this part could be implemented without any of the other parts if so desired. (BGGA would probably find it trickiest to break out just the concept discussed, but it could be done)
The point here is that there are real choices to be made here. The simplicity and understandability of Java must be maintained. But that doesn't mean standing still.
Once again, I'd suggest FCM has the right balance here, and that anyone interested should take a look. Perhaps the right new features for Java are method references and inner methods, and no more?
Opinions welcome as always :-)