Java Control Abstraction
Position paper
Stephen Colebourne, UK
Stefan Schulz, Germany
Ricky Clarkson, UK
Introduction
When there are patterns in code, we seek to eliminate the repeating code, gaining better abstractions and consistency. For many cases, we can use the method as a sufficient abstraction, but when the concept is about flow control, such as the nested resource acquisition pattern, the method is insufficient.
One solution to this problem is closures, where the block to be abstracted is captured together with its enclosing state and passed to a method for processing. With First-class Methods [1], we propose a mechanism for abstracting common code where the code to be abstracted is written by the developer in a manner akin to an inner class. This is a natural approach given that inner classes and object-passing are the approaches used by developers working in Java today.
There is another class of abstraction however, where the rules of First-Class Methods are not applicable. This is where an API is written that is used in a manner closely resembling a built in language keyword.
This document discusses, without going to the detail of a full proposal, some of the issues around control abstraction and its relationship to First-Class Methods.
Control abstraction
First-Class Methods uses the
return keyword to return a result from the inner method to the invoker.
This is the standard Java semantic based on the behaviour of inner classes.
For example, this inner method is invoked for each string in the list. The inner method returns true to the invoking method when the string is short, and the invoking method, removeMatching, will remove the string from the list if it receives a result of true.
StringList strings = ...
strings.removeMatching(#(String str) {
return str.length() < 2;
});
Control abstraction is a different type of syntax however. For example, obtaining and releasing a lock:
Lock lock = ...
withLock(lock) {
// code protected by lock
}
The key differences are as follows:
-
the syntax looks visually similar to a built in keyword (via static import)
-
withLock is a statement, not an expression, so it cannot return a value
-
there is no mechanism to pass a result from the block back to withLock
-
the syntax permitted in the block should be the same as a synchronized block, i.e., any syntax
The effect of these points is that
return should return from the enclosing method, not back to
withLock. And
continue and break should also be permitted to operate across the boundary of the block.
A key requirement of control abstraction is its complete transparency - the block should behave in all ways like a built in statement block (such as synchronized). In particular, any exception thrown within the block will appear transparently as part of the method.
Writing a control abstraction method definition is more complex than writing an inner method method definition. This is because it involves taking into account when the block of code will be run and how return, continue and break and exceptions are handled. It is easy to get these items wrong, and cause unexpected errors in the application code. This is especially true when the abstraction hides multiple threads. Thus a key requirement for control abstraction is to encourage API writers to apply this additional thought process.
Syntax
We would like to propose a syntax for control abstraction that fits our goals. We don't discuss the details of semantics or implementation as there are various choices all of which require detailed investigation.
The objective of the syntax is to meet the requirements in the previous section. This includes allowing exactly one block to the control abstraction definition method.
We considered and rejected the option of using the same syntax as a normal method definition receiving a method type. This could be made to work by enabling the control abstraction if a void return type block was being passed in the last argument
position. Unfortunately, this blocks access to varargs, and in general a position
based syntax is a poor choice. Thus we also ruled out
using the first argument position. In addition, this option did not meet the requirement of encouraging developers to think specifically about the implications of control abstraction.
We also considered and rejected the option of not assigning a variable name to the block being passed to the control abstraction method definition. This option increases the safety of the overall solution, effectively preventing many possible problem areas. However, it removes the ability to perform some important use cases, so was not a viable option.
We also wanted the syntax to be similar to the calling syntax if possible, where the calling syntax will be as per the Java 5 foreach loop. This syntax we have chosen achieves this, and as such also achieves the goals of only allowing one block parameter and being distinct from FCM inner method definitions to encourage additional thought by the API writer.
FileReader example
Using a FileReader (and also other readers and streams) follows a pattern to ensure that once opened, the reader will be closed on normal or abrupt ending of operating on it:
File file = ...;
FileReader reader = null;
try {
reader = new FileReader(file);
// read file
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// ignore
}
}
Employing Java Control Abstraction, the pattern can be captured and used as follows:
usingFileReader(FileReader reader : file) {
// read file
}
This example shows a nested control abstraction which nests a given block providing a specific environment for the block's execution, i.e., the block will be executed at maximum once following the control flow of the application.
This would be implemented by a method, typically static, elsewhere in the system:
public static void usingFileReader(#(void(FileReader)) block : File file) throws IOException {
FileReader reader = null;
try {
reader = new FileReader(file);
block.invoke(reader);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// ignore
}
}
}
The method definition consists of two parts. The first half consists of exactly one parameter which must be an FCM method type with a void return type. The second half, after the colon, are the remaining parameters to the definition. The example shows how these map onto the application code. The block is invoked in the same manner as an FCM inner method.
Map iteration example
The following is a typical pattern on how to iterate in a map's entries using the for-each loop:
Map<Long,Person> map = ...;
for (Map.Entry<Long, Person> entry : map) {
Long id = entry.getKey();
Person p = entry.getValue();
// some operations
}
The Java Control Abstraction mechanism provides means to capture this pattern and use it as follows:
Map<Long,Person> map = ...;
for each(Long id, Person p : map) {
// some operations
}
The example shows an
iteration control abstraction (introduced by the
for keyword) which would be treated similar to iteration statements with respect to using
break,
continue, and
return.
This would be implemented by a method, typically static, elsewhere in the system:
public static <K, V> void each(for #(void(K, V)) block : Map<K, V> map) {
for (Map.Entry<K, V> entry : map.entrySet()) {
block.invoke(entry.getKey(), entry.getValue());
}
}
Here, the method definition defines the single method type parameter
block, which is invoked by
block.invoke(). The keyword
for is used to allow the abstraction to act similarly to common iteration statements, where
break within the block will break the abstraction and
continue will transfer control to the innermost loop of the abstraction code.
Synchronous, threaded execution example
Swing's
invokeAndWait() is used to synchronously execute some task on the AWT event dispatching thread and waiting for that task being processed before continuing. A typical pattern would look like follows:
Runnable doSomething = new Runnable() {
void run() {
// do stuff
}
};
try {
SwingUtilities.invokeAndWait(doSomething);
} catch (Exception e) {
e.printStackTrace();
}
Using Java Control Abstraction, a control like statement can be provided capturing this pattern to allow applying it like so:
SwingUtilities.waitUntilDone() {
// do stuff
}
This, again, would be implemented by an abstraction method, most probably in SwingUtilities, too:
public static void waitUntilDone(#(void()) block : ) {
try {
SwingUtilities.invokeAndWait(block);
} catch (Exception e) {
e.printStackTrace();
}
}
Here, the method definition defines that the block is of the simple method type
#(void()). The block gets assigned to
invokeAndWait(Runnable) employing auto-conversion from the method type to an implementation of the single-abstract method interface
Runnable, providing a single parameterless method
run() (see [1.IV.B] for details on auto-conversion).
Implementation
It is not the aim of this document to prescribe the implementation option.
References
-
S. Colebourne, S. Schulz: "First-class Methods: Java-style Closures (v0.5)"
http://docs.google.com/Doc?id=ddhp95vd_0f7mcns