Record Builder

This example demonstrates the Record Builder pattern in Roc. This pattern leverages the functional programming concept of applicative functors, to provide a flexible method for constructing complex types.

The Basics

Let's assume we want to develop a module that supplies a type-safe yet versatile method for users to obtain user IDs that are guaranteed to be sequential. The record builder pattern can be helpful here.

Note: it is possible to achieve this sequential ID mechanism with simpler code but more "real world" record builder examples may be too complex to easily understand the mechanism. If you want to contribute, we would love to have a real world record builder example that is well explained.

Defining Types

ID : U32

We define a type alias ID which we set to a 32 bit unsigned integer. A type alias improves readability. Otherwise we'd have a bunch of functions that work with U32 where you'd need to look at the context to figure out what the type actually represents. If you ever want to switch your ID's to use a U64, you would only need to change one line!

We want to protect our ID counter, other modules should not be able to alter it, otherwise two users may end up with the same ID! For this protection we use an opaque type that will also accumulate our state:

IDCount state := (ID, state)

This type takes a type variable state. In our case state will be either a record or a function that produces a record. := is used to define an opaque type. (ID, state) is a tuple of an ID(=U32) and our state type variable.

End Goal

It's useful to visualize our desired result. The record builder pattern we're aiming for looks like:

expect
    { aliceID, bobID, trudyID } = 
        initIDCount {
            aliceID: <- incID,
            bobID: <- incID,
            trudyID: <- incID,
        } |> extractState

    aliceID == 1 && bobID == 2 && trudyID == 3

This generates a record with fields aliceID, bobID, and trudyID, all possessing sequential IDs (= U32). Note the slight deviation from the conventional record syntax, using a : <- instead of :, this is the Record Builder syntax.

Under the Hood

The record builder pattern is syntax sugar which converts the preceding into:

expect
    { aliceID, bobID, trudyID } =
        initIDCount (\aID -> \bID -> \cID -> { aliceID: aID, bobID: bID, trudyID: cID })
        |> incID
        |> incID
        |> incID
        |> extractState

    aliceID == 1 && bobID == 2 && trudyID == 3

To make this work, we will define the functions initIDCount, incID, and extractState.

Initial Value

Let's start with initIDCount:

initIDCount : state -> IDCount state
initIDCount = \advanceF ->
    @IDCount (0, advanceF)

initIDCount initiates the IDCount state value with the ID (= U32) set to 0 and stores the advanceF function, which is wrapped by @IDCount into our opaque type.

Applicative

incID is defined as:

incID : IDCount (ID -> state) -> IDCount state
incID = \@IDCount (currID, advanceF) ->
    nextID = currID + 1

    @IDCount (nextID, advanceF nextID)

incID unwraps the argument @IDCount (currID, advanceF); calculates a new state value nextID = currID + 1; applies this new value to the provided advanceF function @IDCount (nextID, advanceF nextID); returning a new IDCount value.

If you haven't seen this pattern before, it can be difficult to grasp. Let's break it down and follow the type of state at each step in our builder pattern.

initIDCount (\aID -> \bID -> \cID -> { aliceID: aID, bobID: bID, trudyID: cID }) # IDCount (ID -> ID -> ID -> { foo: ID, bar: ID, trudyID: ID  })
|> incID                                           # IDCount (ID -> ID -> { aliceID: ID, bobID: ID, trudyID: ID })
|> incID                                           # IDCount (ID -> { aliceID: ID, bobID: ID, trudyID: ID })
|> incID                                           # IDCount ({ aliceID: ID, bobID: ID, trudyID: ID })
|> extractState                                    # { aliceID: ID, bobID: ID, trudyID: ID }

Above you can see the type of state is advanced at each step by applying an ID value to the function. This is also known as an applicative pipeline, and can be a flexible way to build up complex types.

Unwrap

Finally, extractState unwraps the IDCount value and returns our record.

extractState : IDCount state -> state
extractState = \@IDCount (_, finalState) -> finalState

In our case, we don't need the ID count anymore and just return the record we have built.

Full Code

interface IDCounter
    exposes [
        IDCount,
        initIDCount,
        incID,
        extractState,
    ]
    imports []

ID : U32

IDCount state := (ID, state)

initIDCount : state -> IDCount state
initIDCount = \advanceF ->
    @IDCount (0, advanceF)

incID : IDCount (ID -> state) -> IDCount state
incID = \@IDCount (currID, advanceF) ->
    nextID = currID + 1

    @IDCount (nextID, advanceF nextID)

extractState : IDCount state -> state
extractState = \@IDCount (_, finalState) -> finalState

expect
    { aliceID, bobID, trudyID } =
        initIDCount {
            aliceID: <- incID,
            bobID: <- incID,
            trudyID: <- incID,
        }
        |> extractState

    aliceID == 1 && bobID == 2 && trudyID == 3

Output

Code for the above example is available in IDCounter.roc which you can run like this:

% roc test IDCounter.roc

0 failed and 1 passed in 698 ms.