Each year, the F# programming community creates an advent calendar of blog posts, coordinated by Sergey Tihon his blog.
I really like the idea, and have taken part in 2016, 2015 & 2014.
Below is this year's post.
The plan: speed read Christmas
So; you want to find out what Christmas is about, where it really came from… but you don't have much time.
The solution is obvious: take the famous bible passages that churches read every year, and speed read them!
Let's build an app to help us with that.
The tools: Fable and Elmish
Fable is a F# to JavaScript compiler, and Elmish is a library for it designed to provide a Elm/Redux style workflow around it.
If you haven't used Elm or Redux before, the basic idea is that our application will be based around three things:
- A state type. This type will contain all of the information about the state of the application at any moment
- A message type. This will be a discriminated union with a case for each type of "message" that can update the state of the application.
- An update function. This is called every time a message is triggered; it takes the previous state and the message that has just arrived, and produces a new state.
These three things are all we need to manage the state of the application, but then we end up needing one final concept: subscribers.
Subscribers can take the current state, but more importantly they are passed a "dispatch" function that allows them to dispatch messages to the applications message queue. This is how we deal with all inputs in an Elmish application, whether from a user or whether it's things like network requests completing and delivering information our application needs.
The main, most important subscriber is the "view" (i.e. how we're going to show things to the user). In our app, our view will be displayed via a Fable wrapper for React, creating a single page web application. The view is nearly always capable of also dispatching messages - this is how we model things like buttons the user can click on.
You can find more about this, with pretty diagrams, on the Fable Elmish website linked above.
Getting started
Let's start by setting up the application framework. We'll need dotnet core installed, and node with a reasonably recent version of yarn if you want to follow along at home.
Make yourself a new directory, and then on the command line you can run the following commands:
1
|
|
Installs the Fable template for dotnet core.
1
|
|
Creates a new Fable project in this directory, using the directory name for the project name.
1 2 |
|
Download all the basic dependencies, both for dotnet and JavaScript.
Adding our dependencies
Apart from using Fable itself, we also want to make use of Elmish and it's React plugin.
Add these two libraries to paket.dependencies:
1 2 |
|
Then in the src directory add them to our Fable project as well (in paket.references):
1 2 |
|
Run a paket install to download and add the dotnet parts of the libraries to your project:
1
|
|
Then go into the "src" directory and add the JavaScript libraries that these Fable libraries depend on in the browser.
1 2 3 |
|
Setting up the webpage
Let's adapt our HTML, in the "public" folder. The Fable template project assumes that we're going to be using a canvas. We're writing a text only application, so we'll just replace the canvas node with a standard div
and mark it with an id which we'll use to tell react where to render the html our code will generate.
Your index.html should end up looking like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
We're going to speed read by displaying each word of the text really big in the middle of the screen one by one (so that you don't need to move your eyes to read).
Add in a index.css
file with the following to set up styles for a large centered container and a class for displaying really large text.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Writing "JavaScript"
Fable compiles F# to JavaScript, and comes with tooling to watch your code and update it automatically.
Fire up yarn by going into your "src" directory and running:
1
|
|
This will start the fable compiler and keep it running in the background.
We've already decided we want to use Elmish with the React view. We're also going to be loading some external data so we'll want access to the Fetch API.
Let's open up all the namespaces which might be relevant:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Then we need a model; this holds all of the state of our app. The text to be speed read will be stored as an array of strings; we'll keep a Max
field with the index of the last word to make our logic nice and explicit, the Index
of the word currently being displayed, the number of ticks SinceLast
time we updated the word and the current number of TicksPerUpdate
.
1 2 3 4 5 6 |
|
The Msg
type represents all the ways that our app can be updated. The user can ask for the text to become faster, or slower; we can finish loading the text via a web request; and a Tick
of our timer can go past.
1 2 3 4 5 |
|
And the actual update logic takes one of those messages and a previous state, and gives us a new state:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
I was feeling a bit silly, so you can make the application go "so fast it goes backwards." I mean, I've had user requirements that make less sense than that before!
Having defined our types and abstract logic, we now need to write the actual functionality of our app, working our way up to a method which starts it off with an initial state.
First some low level grunge for downloading the text we want to read.
We'll need a url and an auth token for the API we're using (esv.org provide a really nice API by the way).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
We've split it up over multiple lines to make it readable as I'm specifying a lot of options. Nearly all of the them boil down to removing optional metadata from the text (such as verse numbers and translation footnotes). For speed reading we just want the actual words. If you want to run this application a lot, you'll need to register your application on esv.org to get your own auth token.
The text it tries to download is John 1; it's one of the most famous Christmas texts, but also very poetic in it's presentation. I love it, but if you just want "the Christmas story" try a base url of "https://api.esv.org/v3/passage/text/?q=Luke%201-Luke%202:21"
instead.
Now, some boiler plate to extract the passage from the JSON blob that esv.org send back to us. I'm totally ignoring any errors that might occur in the request here, you probably don't want to do that in a real application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
So getText
will, when passed a dispatch
function, call our Url, get the text of he body, throw away everything apart from the text of the passage we actually requested, and then split the passage on any whitespace.
We also want regular ticks
coming through and prompting us to move onto the next word (or the previous if we're going backwards…).
1 2 |
|
Next up, we need our view. The view will both receive new versions of the model as they are created, but will also receive a dispatch functions so it can feed new messages into our update
function.
1 2 3 4 5 6 7 8 9 10 11 |
|
It displays a placeholder while we're loading data, and then buttons to speed up and slow down the speed reading rate.
Finally, we can fire up our application.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
We just set our initial state and then tell react which element in our html we want to render our view in. Because we are registering getText
and triggerUpdate
as subscriptions, they will be passed a dispatch
function and kicked off immediately, so the first thing our app will do is try and download the text.
Once the text is loaded, we'll start going forwards through the text, and are buttons for reading faster and slower will be displayed.
Let's see it in action:
And there we have it - I hope you'll enjoy this brief trip into writing user interfaces in F#, and your speedy recap of one of the most famous readings from the Christmas story!
Appendix: The full App.fs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
|