UI programming with Elmish in F#

This post is the fourth part in a short series on programming web-enabled, functional-friendly applications in F# and has kindly been written by Steffen Forkmann. Thanks, Steffen!

Introduction

The "Model View Update" (MVU) architecture was made famous by the front-end programming language Elm and can be found in many popular environments like Redux. Today it's probably the most famous UI pattern in functional programming. The reason for this is that it clearly separates state changes from UI controls and provides a nice and easily testable way to create modular UIs. Especially for developers that are already familiar with concepts like CQRS or event sourcing many of the things in the MVU architecture will feel natural. Here's what the MVU pattern looks like:

Elmish for F# is a project that provides the capability for client-side apps written in F# to follow the MVU architecture. It comes in multiple flavors, so that you can choose between a React renderer for HTML, React Native renderer for mobile apps and a WPF renderer for desktop applications. In this article we will explore its power and modularity by working through a simple example.

A simple counter

Let's start to illustrate Elmish and the MVU architecture with the very common HTML button counter sample. The following code shows a counter implemented in HTML and JavaScript.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
<html>
    <body>
    <button onclick="--counter; update();">-</button>
    <div id="counter"></div>
    <button onclick="++counter; update();">+</button>
    <script>
        var counter = 0;

        function update() { 
        document.getElementById("counter").textContent = "" + counter;
        }

        update();
    </script>
    </body>
</html>

The code is very straight forward and works as intended, but it has a number of issues:

  1. We are mutating the global variable counter - almost always a dangerous thing to do
  2. We are directly mutating a DOM element, coupling our "business logic" to the UI.
  3. We are referencing the DOM element with its name via a string, which is fragile and can lead to costly knock-on effects.
  4. We've embedded some "domain logic" directly in the onclick event

These issues prevent us from using this in a modular way. For example, if we wanted to create a list with counters, we could not reuse this code. Instead, we could copy & paste the code a couple of times to create a fixed number of counters, but even then we would need to be very careful that we fix all the references to the corresponding global variables. In the object-oriented world, there are number of patterns that allow you encapsulate this problem of shared mutable state - let's see how to do it in a manner that promotes some FP core practices using the three parts of the MVU pattern:

Model

Let's start with the Model.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
type Model = int

type Msg =
| Increment
| Decrement

let init() : Model = 0

In this F# code we capture the current value of the counter in a domain type , before creating a message type which can signal that we want to increment or decrement the counter. We also implement an init function that allows us to create the initial model for our application.

View

The View part deals with the question of displaying controls on the screen. This is where we need to decide on a UI framework; in our case we've decided to stick with HTML, and so we will use the React renderer.

1: 
2: 
3: 
4: 
5: 
let view model dispatch =
    div []
        [ button [ OnClick (fun _ -> dispatch Decrement) ] [ "-" ]
          div [] [ model.ToString() ]
          button [ OnClick (fun _ -> dispatch Increment) ] [ "+" ] ]

This is valid F# code that uses the excellent Fable.React bindings to convert from F# into React JS. The syntax is still similar to our HTML version from the beginning, but instead of < and >, we are using [ and ]. This syntax is easy to learn and IDE tools like Ionide provide code completion for it, so it's a natural fit for existing F# developers. An important observation is that we are no longer mutating state from within the OnClick handlers; instead we simply dispatch one of the earlier defined messages into the system. This decouples our model from the view.

Update

In the Update part, we define a state machine that represents our domain logic (or at least hooks into it).

1: 
2: 
3: 
4: 
let update (msg:Msg) (model:Model) : Model =
    match msg with
    | Increment -> model + 1
    | Decrement -> model - 1

Here, we have defined an update function that will be called by Elmish whenever a message is received. In this very basic scenario there are only two cases to handle, and we can't really see the power of F#'s pattern matching yet. The most interesting observation is that we don't rely on any mutable state. Instead the update function takes a message and the current model, and returns a completely new version of the model. Since all the data is simply inside the model, this is very well testable - we can easily write a set of unit tests around the update function.

The glue

So far, we've not used any functionality from Elmish at all. The Model and Update parts are pure F# code, whilst the View part uses the Fable.React package. At this point all three parts are completely independent from each other - now Elmish will "wire" these up into an application:

1: 
2: 
3: 
4: 
5: 
Program.mkSimple Counter.init Counter.update Counter.view
|> Program.withConsoleTrace
|> Program.withDebugger
|> Program.withReact "counter-app"
|> Program.run

Elmish's Program abstraction provides "glue" functions like mkSimple to bind the three parts together into an application. Internally, Elmish gives us a message loop and takes care of dispatching messages between the view and the update function. Since the complete state is captured in the model and every state change is explicit by processing a message, this opens a whole new world of debugging features like time travelling.

Parent-child composition

In the last section we saw how to build a very basic sample app in the MVU architecture. For this minimal example it's not very clear what the benefit is compared to the original implementation that was using mutation. So, in the following example we'll use the counter as a module and create a list of counters.

Composed Models

As before let's start with the model:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
module CounterList
type Model = Counter.Model list

type Msg = 
| Insert
| Remove
| Modify of int * Counter.Msg

let init() : Model =
    [ Counter.init() ]

In this case we have a list of counter models and a new message type. We can signal to:

  • Insert a new counter
  • Remove the latest counter
  • Dispatch a counter message to the corresponding counter in the list.

When the application starts we will start with one counter as provided by the init function. As you can see, every part of the model refers to the corresponding part of the submodel. CounterList.Model is a collection of Counter.Model, the CounterList.Msg uses the Counter.Msg as payload and the CounterList.init function calls the Counter.init function.

Composed Views

Let's take a look at the View code:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
let view model dispatch =
    let counters =
        model
        |> List.mapi (fun pos counterModel -> 
            Counter.view
                counterModel
                (fun msg -> dispatch (Modify (pos, msg)))) 
    
    div [] [ 
        yield button [ OnClick (fun _ -> dispatch Remove) ] [ "Remove" ]
        yield button [ OnClick (fun _ -> dispatch Insert) ] [ "Add" ]
        yield! counters ]

It's not that much different to the view code of the counter itself, except now we render a list of counters and wrap the messages with position information. This code may look a bit unfamiliar at this point, but the F# compiler is helping us here. There is only one way to get it to compile - and that's the correct way! Once you've done this step a few times, it becomes second nature - just like any task that you become familiar with.

Composed Updates

As with the Model, we also see a nice symmetry in the Views. But what about the Update part?

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
let update (msg:Msg) (model:Model) =
    match msg with
    | Insert ->
        Counter.init() :: model // append to list
    | Remove ->
        match model with
        | [] -> []              // list is already empty
        | x :: rest -> rest     // remove from list
    | Modify (pos, counterMsg) ->
        model
        |> List.mapi (fun i counterModel ->
            if i = pos then
                Counter.update counterMsg counterModel
            else
                counterModel) }

Now this may come as no surprise, but we have the same situation here: CounterList.update forwards to Counter.update of the corresponding counter! We end up with something really beautiful, since now we have a CounterList component which exposes exactly the same elements as the Counter itself, namely Model, View and Update. This allows us to use the CounterList itself as a component!

F# - a great fit for the MVU architecture

The MVU architecture was made famous by the Elm language, but can be used in many languages, even in vanilla JavaScript. F# - like Elm - is a language in the ML family of programming language, and provides features such as pattern matching that are extremely powerful and particularly useful for modelling state machines. In conjunction with F# features such as Discriminated Unions, this allows us to create applications in a type-safe manner that cater for all possibilities. The compiler provides us with guidance when corner cases are not dealt with, leading to quicker development time, whilst allowing F# developers to rapidly create UIs in a typesafe manner. The larger and more complex an application becomes, the greater the benefit; the F# compiler (like the Elm compiler) emits warnings for us when we forget to handle all possibilites - even for more complicated patterns - meaning less time spent debugging or writing unit tests, and more time delivering business value.

More Information

For more information about Elmish and the MVU architecture please see the following resources:

Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member GetSlice : startIndex:int option * endIndex:int option -> 'T list
  member Head : 'T
  member IsEmpty : bool
  member Item : index:int -> 'T with get
  member Length : int
  member Tail : 'T list
  static member Cons : head:'T * tail:'T list -> 'T list
  static member Empty : 'T list

Full name: Microsoft.FSharp.Collections.List<_>
val mapi : mapping:(int -> 'T -> 'U) -> list:'T list -> 'U list

Full name: Microsoft.FSharp.Collections.List.mapi