Elm architecture for F# console apps

Friday, July 1, 2016

At the most recent @FunctionalKnox meetup a question came up about the Elm architecture and how that might be implemented to develop a console application in F#. As it turns out it's not only feasible but actually quite terse.

The main parts of the architecture are the Model to hold the current state of the application, the Message to define what actions are available, the Update function which takes the current state and a message and returns a new state, and the View function which displays the current state of the application. The runtime then orchestrates all of those pieces to create an application. The beauty here is that everything is immutable and all side effects take place at the very edge of your application.

Without further ado, let's start building. For this simple example we'll build a simple counter that can be incremented or decremented.

The Model

In this case our model is just an int that is either incremented or decremented so Model is just a type alias for int. We'll also define our initial state so we know where to start.

type Model = int
let init : Model = 0

The Message

We want to allow the user to Increment or Decrement our Model so our Message needs to be a sum type.

type Message = 
    | Inc
    | Dec

The Update Function

In the Elm Architecture, the Update function takes a Model and a Message and returns a new Model. Model -> Message -> Model. This is where exhaustive pattern matching really shines because we're forced to handle every possible Message.

let update (model : Model) (msg : Message) =
    match msg with
    | Inc -> model + 1
    | Dec -> model - 1

If the incoming Message is Inc we increment our Model and if its Dec we decrement it.

The View Function

Elm is written for the web which has a ton of different display options. In our case we're only targeting the console so our options are greatly constrained. In this case our view takes a Model and returns a string list

let view (model : Model) =
    [ "The current value is:"
      model |> string ]

The Parse Function

This isn't exactly part of the Elm Architecture, but it's the way I'm handling input from the user. The Parse function is a single partial active pattern that will be used by our main loop. It takes a string and returns a Message option.

let (|Parse|_|) = function
    | "Inc" -> Some Inc
    | "Dec" -> Some Dec
    | _ -> None

The Main Function

With all of our pieces defined we can now write a recursive function that renders our view, accepts input, performs updates, and allows the user to exit the program. Because view is just a function from Model to string list, rendering the UI is a simple piping operation.

model |> view |> Seq.iter (printfn "%s")

Next we need to accept user input and try to map that to a Message. If no match is found we look for an "x" which will exit the program. Otherwise we simply recurse.

match System.Console.ReadLine() with
| Parse msg -> main view (update model msg)
| "x" -> ()
| _ -> main view model

If we put that together we have a recursive main function that will run our application for us.

let rec main view model =
    model |> view |> Seq.iter (printfn "%s")
    match System.Console.ReadLine() with
    | Parse msg -> main view (update model msg)
    | "x" -> ()
    | _ -> main view model

All the Code

All told I ended up with about 29 lines of code for our simple application. In this trivial case that may be overkill, but the fact that we're fully immutable and have all side effects pushed to the boundaries of our application means all of our business domain and logic can be pure functions which is a huge win from my experience.

type Model = int

type Message = 
  | Inc
  | Dec

let update (model : Model) (msg : Message) =
  match msg with
  | Inc -> model + 1
  | Dec -> model - 1

let init : Model = 0
  
let view (model : Model) =
  [ "The current value is:"; model |> string ]
  
let (|Parse|_|) = function
  | "Inc" -> Some Inc
  | "Dec" -> Some Dec
  | _ -> None

let rec main view model =
  model |> view |> Seq.iter (printfn "%s")
  match System.Console.ReadLine() with
  | Parse msg -> main view (update model msg)
  | "x" -> ()
  | _ -> main view model

main view init