Functional builders in Java with Jilt
A few months ago, I shared an article about what I called Java functional builders, inspired by an equivalent pattern found in Go. The main idea was to have builders that looked like this example:
LanguageModel languageModel = new LanguageModel(
name("cool-model"),
project("my-project"),
temperature(0.5),
description("This is a generative model")
);
Compared to the more tranditional builder approach:
- You’re using the
new
keyword again to construct instances. - There’s no more
build()
method, which felt a bit verbose.
Compared to using constructors with tons of parameters:
- You have methods like in traditional builders, that say what each parameter is about (
name()
,temperature()
…) a bit similar to named parameters in some programming languages.
The approach I followed was to take advantage of lambda functions under the hood:
public static ModelOption temperature(Float temperature) {
return model -> model.temperature = temperature;
}
However, there were a few downsides:
- Of course, it’s not very conventional! So it can be a bit disturbing for people used to classical builders.
- I didn’t make the distinction between required and optional parameters (they were all optional!)
- The internal fields were not
final
, and I felt they should be.
Discovering Jilt
When searching on this topic, I found Adam Ruka’s great annotation processor library: Jilt.
One of the really cool features of Jilt is its staged builder concept, which makes builders very type-safe, and forces you to call all the required property methods by chaining them. I found this approach very elegant.
Adam heard about my functional builder approach, and decided to implement this new style of builder in Jilt. There are a few differences with my implementation, but it palliates some of the downsides I mentioned.
Let’s have a look at what functional builders looks like from a usage standpoint:
LanguageModel languageModel = languageModel(
name("cool-model"),
project("my-project"),
temperature(0.5),
description("This is a generative model")
);
Compared to my approach, you’re not using constructors (as annotation processors can’t change existing classes), so you have to use a static method instead. But otherwise, inside that method call, you have the named-parameter-like methods you’re used to use in builders.
Here, name()
, project()
and temperature()
are mandatory, and you’d get a compilation error if you forgot one of them.
But description()
is optional and can be ommitted.
Let’s now look at the implementation:
import org.jilt.Builder;
import org.jilt.BuilderStyle;
import org.jilt.Opt;
import static jilt.testing.LanguageModelBuilder.*;
import static jilt.testing.LanguageModelBuilder.Optional.description;
//...
LanguageModel languageModel = languageModel(
name("cool-model"),
project("my-project"),
temperature(0.5),
description("This is a generative model")
);
//...
@Builder(style = BuilderStyle.FUNCTIONAL)
public record LanguageModel(
String name,
String project,
Double temperature,
@Opt String description
) {}
I used a Java record
but it could be a good old POJO.
You must annotate that class with the @Builder
annotation.
The style
parameter specifies that you want to use a functional builder.
Notice the use of the @Opt
annotation to say that a parameter is not required.
Derived instance creation
Let me close this article with another neat trick offered by Jilt, which is how to build other instances from existing ones:
@Builder(style = BuilderStyle.FUNCTIONAL, toBuilder = "derive")
public record LanguageModel(...) {}
//...
LanguageModel derivedModel = derive(languageModel, name("new-name"));
By adding the toBuilder = "derive"
parameter to the annotation, you get the ability to create new instances
similar to the original one, but you can change both required and optional parameters, to derive a new instance.
Time to try Jilt!
You can try functional builders in Jilt 1.6 which was just released a few days ago!