JavaScript-free Web Development with WebAssembly, Bolero, and F#

A while back, I remember hearing about WebAssembly and how it would finally release the web from the grip of JavaScript. (Languages that transpile to JavaScript don’t cut it).

Although WebAssembly doesn’t make the news much these days, it turns out there is quite a bit of work being done in this area. Since I do a fair amount of back-end work in F#, I was looking for a way to use F# client-side, and that’s when I discovered Bolero.

Bolero

Bolero combines work from Fable (F#-to-JavaScript transpiler) and Blazor (WebAssembly for C#) to enable seamless F# development both server-side and client-side. The front-end architecture is borrowed from Elm. It’s even called Elmish.

“Elm Architecture” is remarkably simple, and it can be summarized as Model, View, Update. The model is an immutable data structure that stores state. The view is rendered from the model. And updates are the way that user interaction and other events mutate the model (or more accurately, generate a new model).

Bolero is really easy to get started, so I won’t give a detailed how-to here. Instead, I’ll focus on my first impressions.

Buidling with Bolero

I decided to take Bolero for a spin by writing a simple multiple-choice quiz application. It would include a list of questions, each with a list of possible answers. One answer would be designated as the correct one, and the list would be shuffled for display. A “Next Question” button would provide navigation throughout the quiz.

With these requirements in mind, I created my model.

Model
type Model =
    {
        questions: Question list
        choices: string list
        answerState: AnswerState
    }

and Question =
    {
        text: string
        correctChoice: string
        incorrectChoices: string list
    }

and AnswerState =
    | Unanswered
    | AnsweredCorrectly
    | AnsweredIncorrectly

The idea here is that the list of questions is initialized at startup, and the current question is always the one at the head of the list. The list of current choices is stored separately so they can be shuffled.

Although this model does a good job of representing my state, there are still opportunities to make impossible states unrepresentable. I know this was a major theme with Elm, and it is well within reach of functional programming in general. But compared to all the checks that would be necessary to even begin to accomplish the same thing in JavaScript, we’re already way ahead!

With the model in place, the next thing I need is a type to represent the available update operations, and a function to process them.

Update

type Message =
    | ScrambleChoices
    | NextQuestion
    | Answer of string

let update message model =
    match message with
    | ScrambleChoices ->
        let question = List.tryHead model.questions
        let choices =
            match question with
            | None -> []
            | Some question ->
                (question.correctChoice :: question.incorrectChoices)
                |> shuffleList
        { model with choices = choices }, Cmd.none

    | NextQuestion ->
        { model with questions = List.tail model.questions
                     answerState = Unanswered }, Cmd.ofMsg ScrambleChoices

    | Answer choice ->
        let question = List.head model.questions
        { model with answerState =
                         if choice = question.correctChoice
                         then AnsweredCorrectly
                         else AnsweredIncorrectly }, Cmd.none

It might seem like there’s a lot going on here, but the same pattern is repeated for each message handler: Figure out what to change in the model, and then return an updated model. One other interesting thing is that the return type is actually a tuple of model and a follow-up command. I’ve used this to immediately scramble the answers when advancing to the next question.

Finally, I have enough to render the view.

View

Bolero has a couple of options for rendering views. One approach is to use HTML templates, as you might expect. However, they’re extremely limited. You can insert so-called holes for simple variable substitution, but that’s it. This is really disappointing, because the templating engine is hooked up using an F# type provider. I could imagine it providing some neat design-time template validation (in fact, if you try to assign to a non-existent template hole, you’ll get a red squiggly).

I understand that there may be a desire to avoid putting everything and the kitchen sink into templates (that’s how you end up with PHP), but a little more functionality would go a long way here. Some basics like conditionals and loops would allow more of the template to be specified in HTML. I wouldn’t go so far as to enable F# code within the template, though; logic and presentation are best kept separate.

So I started out writing my view as a template, but this is as far as I could get:

<div class="quiz">
  <div>${QuestionText}</div>
  <ol class="choices">
    ${Choices}
  </ol>
  <div class="feedback">
    ${Feedback}
  </div>
  <div>
    <button class="next-question" onclick="${NextQuestion}">Next Question</button>
  </div>
</div>

As it is, anything other than simple variable substition must use the alternative, which is HTML-in-F#. It looks like this (since I ended up using a template for the overall structure, and this is providing values for the template holes, it may not be the best example. Focus on the block provided to Choices in the middle):

let view model dispatch =
    Main()
        // Inputs
        .NextQuestion(fun _ -> dispatch NextQuestion)
        // Outputs
        .QuestionText(
            cond (List.tryHead model.questions) <| function
            | None -> text "You have completed the quiz!"
            | Some question -> text question.text)
        .Choices(
            forEach model.choices <| fun choice ->
                li [] [
                    cond model.answerState <| function
                    | Unanswered ->
                        button [
                            attr.``class`` "choice-btn"
                            on.click (fun _ -> dispatch (Answer choice))
                            ] [text choice]
                    | _ -> text choice
                ])
        .Feedback(
            cond model.answerState <| function
            | Unanswered -> empty
            | AnsweredCorrectly -> text "Correct!"
            | AnsweredIncorrectly -> text "Wrong!"
        )
        .Elt()

It’s awkward–essentially building a DOM tree in F# syntax, using special-purpose helper functions, instead of using the domain-specific language of HTML. And that attr.``class``…yikes. Building up a DOM within program code is not a new concept, but it would take some getting used to.

The main advantage of this approach is that you get all of the type safety of F# directly in your UI specification. That’s actually pretty awesome. But it’s really hard to imagine developing a sizable application like this and keeping it maintainable. It would take some practice to develop a formatting scheme that is concise and readable, yet flexible enough for refactoring.

The Future

Despite the rough edges (this stuff is still pretty new), I think it’s exciting to be able to do front-end web development in F#. I’m looking forward to putting the full stack together and having F# all the way down!

Conversation
  • Sultan says:

    Wow! What an amazing article, you touched up on a lot of F# features, and give a very nice generalize article. However, I’m not sure if the article is dated or if bolero made strides after you posted this, but in the View section, you mentioned how HTML templates only has holes, so there is no conditional or loops embedded within the html, like you would with Vue or React, however, you can use the node, and repeat as necessary within F#. So you can essentially by pass building a lot of html straight from within F# by creating many html files, and nesting your templates. The node is very powerful and has been used in Vue.js especially for dynamically injecting html parts at run time.

  • Comments are closed.