Adding keyword arguments to Java with annotation processing
2023-03-27
Java is a language missing a lot of features. One of those missing features is keyword arguments. By that, I mean something that lets you call functions like this:
my_function(x=1, y=2, z=3)
Or even:
my_function(z=3, x=1, y=2)
That is, arguments that are named, can be reordered, and are non-optional at compile time. You might quibble: Python doesn't have compile time. But you can run mypy to check types and if you're missing a required keyword argument, mypy will fail.
Let's limit our scope to constructors, and aim that if given code like this:
package org.example;
@ReorderableStrictBuilder
public record MyBuiltClass(String first, String second, String third) {
}
We want to be able to construct an object something like this:
// Named arguments
var x = Builder.create().setFirst("1").setSecond("2").setThird("3").build();
// Reorderable
var x = Builder.create().setSecond("2").setThird("3").setFirst("1").build();
// Compile time error if you miss out any arguments - this shouldn't compile.
var x = Builder.create().setSecond("2").setThird("3").build();
Is that even possible? I tried to find out. First, let's look at some solutions I don't like.
Errors at runtime - the worst kind of "builder"
The way the builder pattern is usually done is pretty bad. Someone said one time that design patterns are just evidence that the language isn't powerful or expressive enough for the programmer's ideas. Any structure or regularity in the code is repetition, and could be eliminated with a powerful enough language or macro system.
I don't know if that's true, but the builder pattern is usually obviously unsafe or unergonomic. Here's an unsafe or just plainly incorrect example: JAXB code generation.
JAXB is a way to (among other things) generate Java classes from XSD schemas. In an XSD schema you can mark fields as required or optional. JAXB classes can be used in a pseudo-buildery way like so:
var x = new MyClass().withX(1).withZ(2);
But there's a problem - there's no compile-time enforcement of required
parameters. Meaning your schema can have all required types, but you can just
do new MyClass()
and even serialize it. No-one will complain, except at
runtime, maybe. If you're lucky.
Terrible, terrible, we obviously don't want to just delegate all checks to runtime.
The usual builder pattern
I haven't done much research on this, but from various tutorials online e.g. this one from DigitalOcean, you can see something typical probably.
The required arguments are in the constructor:
public ComputerBuilder(String hdd, String ram){
this.HDD=hdd;
this.RAM=ram;
}
You might think this is fine even for a small number of arguments, but they have the same type! You can swap them around by mistake, and it's hard to tell at the call site. Your IDE probably can't help you except by simulating keyword arguments.
You type something like:
var x = new ComputerBuilder(ram, hdd);
and you can't tell it's wrong looking at the file or in the diff. Your IDE (if it's IntelliJ, at least) might annotate this as:
var x = new ComputerBuilder(hdd: ram, ram: hdd);
and you can tell something's up. Fine, but that's showcasing a glaring deficiency in the language that the IDE papers over.
One solution which isn't naming the arguments is to name the types better,
perhaps have an Hdd
class and Ram
class. Maybe a good idea, but it's a lot
of work and introduces a lot of code. A topic for another blog post.
This kind of builder pattern is out - it's not even really a builder pattern, the core is just a plain old Java constructor.
Staged builder
There's another solution which will get us named arguments with a compile time error if we miss anything required - staged builders.
A great example is https://immutables.github.io/immutable.html#staged-builder which can generate staged builder code for you. The way it works is that each builder method returns a new builder type with only one method that lets you set the next required field.
Immutables has a great example:
// under the hood
public final class ImmutablePerson implements Person {
...
public static NameBuildStage builder() { ... }
public interface NameBuildStage { AgeBuildStage name(String name); }
public interface AgeBuildStage { IsEmployedBuildStage age(int age); }
public interface IsEmployedBuildStage { BuildFinal isEmployed(boolean isEmployed); }
public interface BuildFinal { ImmutablePerson build(); }
}
And they can generate that code for you with an annotation. This is fine, the errors are at compile time and you have to name all of your arguments. Maybe this is good enough, but we do lose the reorderability of the arguments. And there are a lot of extra types around.
Let's file that one away and move on.
My attempt at a builder annotation
I saw this StackOverflow answer and it was something I hadn't seen before in Java (I haven't seen all that much Java code). It kind of reminded me of template metaprogramming in C++. The example, as written, is this:
public static void test() {
// Compile Error!
Complex c1 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2"));
// Compile Error!
Complex c2 = new Complex(Complex.Builder.create().setFirst("1").setThird("3"));
// Works!, all params supplied.
Complex c3 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2").setThird("3"));
}
The interface isn't exactly what we wanted, but it's close. The trick is to simulate a non-type boolean template parameter to keep track of which values have been set:
public static class Builder<Has1,Has2,Has3> {
public static class False {}
public static class True {}
private Builder() {}
public static Builder<False,False,False> create() {
return new Builder<>();
}
public Builder<True,Has2,Has3> setFirst(String first) {
this.first = first;
return (Builder<True,Has2,Has3>)this;
}
// ...
}
Then we only expose a constructor for the final result that takes the builder with all type parameters set to true:
public Complex(Builder<True,True,True> builder) {
first = builder.first;
second = builder.second;
third = builder.third;
}
I thought that was pretty clever. Ideally I'd want something like:
public static class Builder<Has1,Has2,Has3> {
if (Has1 && Has2 && Has3)
public Complex build() {
return new Complex(first, second, third);
}
i.e. the build
method only gets exposed after all fields are populated, but
Java can't do that kind of thing. C++ can with e.g. enable_if
.
I wrote an annotation processor here: https://github.com/kaashif/java-keyword-args/ which given:
@ReorderableStrictBuilder
public record MyBuiltClass(String first, String second, String third) {
}
generates this builder code:
public class MyBuiltClassBuilder<HasFirst, HasSecond, HasThird> {
private MyBuiltClassBuilder() {}
private static class True {}
private static class False {}
public static MyBuiltClassBuilder<False, False, False> create() {
return new MyBuiltClassBuilder<False, False, False>();
}
private java.lang.String first;
public MyBuiltClassBuilder<True, HasSecond, HasThird> setFirst(java.lang.String arg) {
this.first = arg;
return (MyBuiltClassBuilder<True, HasSecond, HasThird>) this;
}
private java.lang.String second;
public MyBuiltClassBuilder<HasFirst, True, HasThird> setSecond(java.lang.String arg) {
this.second = arg;
return (MyBuiltClassBuilder<HasFirst, True, HasThird>) this;
}
private java.lang.String third;
public MyBuiltClassBuilder<HasFirst, HasSecond, True> setThird(java.lang.String arg) {
this.third = arg;
return (MyBuiltClassBuilder<HasFirst, HasSecond, True>) this;
}
public static MyBuiltClass build(MyBuiltClassBuilder<True, True, True> builder) {
return new MyBuiltClass(builder.first, builder.second, builder.third);
}
}
Which seems to give us everything we wanted, except that the API is a little ugly:
MyBuiltClass c1 = MyBuiltClassBuilder.build(MyBuiltClassBuilder.create().setFirst("1").setSecond("2").setThird("3"));
It's really not that bad of a tradeoff given that we can enforce at compile-time that all setters must be called at least once.
The worst part of this process was using JavaPoet to generate this code. For those unfamiliar, Java annotation processors output text - they're barely a level above C preprocessor macros. JavaPoet is a library that helps with that. It was easier to just generate the text myself.
The billion dollar elephant in the room
Oh, we have compile-time enforced required parameters do we? WRONG! You can
still do something like builder.setFirst(null)
and no-one can stop you! This
is sad.
No builder pattern will save you from null in Java. Switch to Kotlin.
Conclusion
I think you should probably just use Immutables - it's a great library with a great staged builder annotation. I think that's the best off-the-shelf solution even if it does mean we lose reorderability of arguments.
Writing an annotation processor in Java isn't that painful actually. Take a look at the code! https://github.com/kaashif/java-keyword-args/ This is my first time doing anything like this in Java, it was fun to learn about.