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.
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
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 -> Message -> Model. This is where exhaustive pattern matching really shines because we're forced to handle every possible
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
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
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
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