Paul Hammant's Blog:
Another feature for the Java language
A year ago, I talked about this method-centric language feature in a blog entry That Ruby and Groovy Language Feature. A year before that I rambled around More syntactic sugar for java DSLs. This blog entry imagines another DSL enhancement to Java.
The Imagined DSL Feature
Consider a method-centric DSL feature for regular Java. Here’s what that could look like (this won’t compile today):
package com.example;
import java.util.ArrayList;
import java.util.List;
import static com.example.App.Outer.outer;
public class App {
public static void main(String[] args) {
Outer outr = outer() {
// I'd want all of Outer's constructor to be executed and the 'this' to be
// on the Outer of the return statement
System.out.println("Should say com.example.Outer@hexhexhex1" + this);
middle() {
// Implicit call of middle method on Outer instance
System.out.println("Should say com.example.Middle@hexhexhex2" + this);
inner() {
// Implicit call of inner method on Middle instance
System.out.println("Should say com.example.Inner@hexhexhex3" + this);
}
}
}
// Out of the method-centric DSL, and back into regular OO Java...
// Subject to end-user implementation, we could walk the tree doing something
// else that is worthwhile via the "outr" var:
outr.depthFirstRecurse();
}
}
You would write the DSL classes for that like so:
public static class Outer {
private List<Middle> middles = new ArrayList<>();
public static Outer outer() {
return new Outer();
}
public Middle middle() {
Middle middle = new Middle();
middles.add(middle);
return middle;
}
// other dsl-ish methods
public void depthFirstRecurse() {
System.out.println("Processing Outer");
middles.forEach(Middle::depthFirstRecurse);
}
}
public static class Middle {
private List<Inner> inners = new ArrayList<>();
public Inner inner() {
Inner inner = new Inner();
inners.add(inner);
return inner;
}
public void depthFirstRecurse() {
System.out.println("Processing Middle");
inners.forEach(Inner::depthFirstRecurse);
}
}
public static class Inner {
public void depthFirstRecurse() {
System.out.println("Processing Inner");
}
}
}
Of course, we already have double-brace initialization in Java, but only for instantiation of classes and not method-centric.
list1 = new ArrayList<String>() {{
// the 'this' is the new ArrayList<String>() above, after instantiation, but before it is assigned to var 'list1'
add("first");
add("second");
}};
// list1 has two entries.
There is also a much less pretty way with lambdas you could use today.
Historical Perspective: 1997 JVM tricks
There was a moment in time in 1997 when Inner Classes were added to Java for v1.1 in such a way that v1.0.2 JVMs could still execute them even if the javac of v1.0.2 could not compile inner classes. Inner classes appeared as additional .class files with a $ in their name and if decompiled, you could see how it all worked without being a revolution. There were some political reasons for the backward compatibility - Microsoft was in dispute with Sun on Java’s directions, and their JVM was one place that played out
New generated code for our method-centric DSL:
For our imagined language feature, Javac could also generate some classes during compilation and weave the intended execution into the resulting .class files. Same as what happened in 1997, albeit a little more complicated.
These new classes would allow developers to define method-centric code blocks that can be executed in a structured manner. Here’s a conceptual example of what the invisibly added classes might look like if decompiled for our example:
// Generated by javac compiler - this was a static method invocation.
package com.example;
public class Outer$outer implements Runnable {
private final Outer outer;
public Outer$outer(Outer outer) {
// Java called Outer.outer before calling this constructor
this.outer = outer;
}
@Override
public void run() {
System.out.println("Should say com.example.Outer@hexhexhex1 " + outer);
Middle middle = outer.middle();
new Outer$middle(middle).run();
}
}
// Generated by javac compiler
package com.example;
public class Outer$middle implements Runnable {
private final Middle middle;
public Outer$middle(Middle middle) {
this.middle = middle;
}
@Override
public void run() {
System.out.println("Should say com.example.Middle@hexhexhex2 " + middle);
Inner inner = middle.inner();
new Middle$inner(inner).run();
}
}
// Generated by javac compiler
package com.example;
public class Middle$inner implements Runnable {
private final Inner inner;
public Middle$inner(Inner inner) {
this.inner = inner;
}
@Override
public void run() {
System.out.println("Should say com.example.Inner@hexhexhex3 " + inner);
}
}
This java compiler feature would enable a more intuitive and readable way to define and execute nested DSL-like method calls, similar to Ruby and Groovy’s way.
Kotlin’s Approach
Kotlin’s DSL support is already built into that language. It is very concise and elegant. Kotlin DSLs allow a “lambda with a receiver object” and you can define them in a fairly natural way. This means with the pseudo-declarative DSL, you can call methods and access properties of the receiver directly without explicit references. For example:
val myOuter = outer {
println("Outer: $this")
middle {
println("Middle: $this")
inner {
println("Inner: $this")
}
}
}
The above is facilitated by very simple classes and functions like so:
fun outer(block: Outer.() -> Unit): Outer {
val outer = Outer()
outer.block()
return outer
}
class Outer {
fun middle(block: Middle.() -> Unit) {
val middle = Middle()
middle.block()
}
}
class Middle {
fun inner(block: Inner.() -> Unit) {
val inner = Inner()
inner.block()
}
}
class Inner
The Kotlin DSL code reads almost like a natural language, with the receiver “this” implicitly passed into the lambda. This reduces boilerplate and improves readability.
Because these DSL constructs are part of Kotlin’s language features, the compiler can enforce type-safety and even provide useful IDE support.
Conclusion
For the Java’s own version of the same, I am unsure where the limits would be. Kotlin was designed with DSLs in mind, making it inherently more suited for these patterns. Java’s evolution in this direction would require significant changes to the language and tooling, even if the JVM could be kept the same. Tooling includes IDEs which have more challenges: adding breakpoints and navigating during debugging, intellisense and all that during editing, as well as code completion.
Would you use it, if Java had it? I would in https://github.com/paul-hammant/tiny at least, if not more.