RESULT BUILDERS AND DSL — X — FILE

Andrea Finollo
5 min readFeb 26, 2022

--

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:

SwiftUI view builder

These line of code are interpreted into this at compile time:

SwiftUI view builder 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

Some abstractions to make our Markdown editor
Our DSL objects
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:

Vanilla result builder implementation

We take the components var-arg, iterate over it and compose a string that is the result of the MarkdownConvertibletransformation.

This will make us able to write code like this:

Vanilla MarkdownDoc

That is cool, but super simple we would prefer to write something more complex and flexible like this

A lot better markdown implementation

First let’s start to enable the use of the if statement, to do that we must add this method:

Builder method to add only IF statement

This method enable only support for if statement without else For if-else other two methods must be implemented :

Builder methods to add IF-ELSE statement

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-elseimplementation actually looks like.

Magic revealed

The loop forneeds, guess what, another method:

Builder methods to add FOR loops

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, switchand 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 .

--

--