A few months ago, I shared an article about what I called Javafunctional 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!
Top comments (0)