RESULT BUILDERS AND DSL — X — FILE
Almost a month ago I started to create a new library called BitWiser with the help of a colleague. It’s a library that helps developer in dealing with bits, bytes and nibbles. Working a lot with bluetooth I’ve found that could be interesting write a DSL close to SwiftUI, but for creating a Data objects. This article will explain how to create your own DSL as I did in BitWiser.
THE MAGIC
Probably you have already seen a piece of code like that, and you know where it does come from.
And probably, like me, you had seen a little bit of magic in SwiftUI.
How is it possible? each line seems an instruction, they are views instantiation and there is no variable holding them.
It seems an array but you can write if
statements and the objects are not separated by commas.
This can be achieved by using result builders (formerly know as function builder).
I STILL DON’ T UNDERSTAND HOW IT WORKS
Here is what happens using a ViewBuilder the heart of SwiftUI:
These line of code are interpreted into this at compile time:
I guess all the magic is lost, but that is exactly what is happening under the hood.
WHY SHOULD I USE THEM?
I think that they find a natural purpose when you need to compose something and when what you are building is the sum of different elements. For the very same reason they are very useful to write a DSL.
For instance imagine a tool to create a CI pipeline, instead of having weird indentation files for configuring it you can have a very well defined pipeline with compile time error reporting.
RESULT BUILDER ANATOMY
Result builder are built on to of three types (actually typealiased types):
- Component: basic building block
- Expression: when you want to make them work with different input types
- Final result: when you want to make builders work with a different type as the final result.
To create a result builder you need at least this statement and the @resultBuilder annotation
As you can see this method receive a variadic argument Component
that results in a Component
.
If you want to implement other functionalities you should add more methods each one has its specific use case and you can see a list of them in the proposal.
LET’ S CREATE A MARKDOWN(-ish) BUILDER
The document builder must support these tags:
- Level One header (# )
- Level Two header (## )
- Level Three header ( ### )
- Body
The output will be shown as a common String
with markdown tags.
Basically we need:
- an interface that can convert a common string into one with the correct markdown tag: MarkdownConvertible
- a serie of objects that can be initialized with a common string on which we must implement the interface above to create a tagged version of the string itself. We will have: LevelOneHeader
, LevelTwoHeader
, LevelThreeHeader
, Body
- an object that represents the markdown document and that can be initialized by using all the pieces above with the help of a result builder, the MarkdownDoc
The @MarkdownCreator is our result builder let’s see how it works.
As I wrote before to create a result builder at minimum we need the annotation @resultBuilder and a method. The first implementation will look like this:
We take the components var-arg, iterate over it and compose a string that is the result of the MarkdownConvertible
transformation.
This will make us able to write code like this:
That is cool, but super simple we would prefer to write something more complex and flexible like this
First let’s start to enable the use of the if
statement, to do that we must add this method:
This method enable only support for if
statement without else
For if-else
other two methods must be implemented :
As you can see we just implement methods to support different conditions, these new methods will be used by the compiler to understand our code correctly. Note that the implementation are really simple, we take as an input the markdown convertible object and transform it.
Here how an if-else
implementation actually looks like.
The loop for
needs, guess what, another method:
The implementation is very similar to the one with the var-args and used to combine all the strings.
Now we can create a document in different situations in an expressible way, we can use if-else
statements, switch
and also for
loops.
We created a DSL that would make easy for anyone to build a markdown(-ish) document.
FINAL THOUGHTS
Result builders are really an awesome feature and as applications growth in complexity they really help in simplifying creating configurations or anything that you can create with a specific DSL. They abstract away complexities and give back focus to the context, the domain and what you are trying to solve.
I used result builder to create a library called BitWiser. I also added a DSL to create a Data object from any object that conforms to a specific protocol. You can do amazing thing with result builder and this is just the tip of the iceberg.
Here a link of a collection of repositories that have use them to create beautiful stuff: awesome result builders .