Getting Started With Incr_dom - Part 1
Note: Incr dom is superceded by a new, much easier to use library called Bonsai
Incr_dom is a library created by Jane Street for building web applications using OCaml. The interesting thing about Incr_dom is it’s built on another one of Jane Street’s libraries, called Incremental. Incremental lets you memoize parts of your application to avoid unnecessary computations. When you combine incremental with something like the virtual-dom you get very speedy web applications. This is the topic of today’s article. We’ll be building a very simple ‘Single Page (Web) Application’ using Incr_dom. The application will be a simple game of Tetris. It’ll include the main menu, a leaderboard viewer, settings page, and of course the game itself.
Prerequisites
You’ll need opam
, installation instructions can be found here. You should install opam 2.0
if possible, as that’s what this tutorial will be using. Next you’ll need to install a recent version of OCaml. This can be done with the following opam
command:
opam switch create 4.07.0
Which will install the 4.07.0 version of the compiler. This will take a bit as opam
downloads and compiles OCaml. Once it’s done run the following command to get the necessary libraries:
opam install dune incr_dom
Now we can use dune (a popular OCaml build system) to create our project:
dune init project incr_dom_tetris
Next we need to modify the lib/dune
file to add our dependencies, it should look like the below:
(library
(name incr_dom_tetris)
(libraries
incr_dom
)
(preprocess (pps ppx_jane))
)
It should be fairly self-explanatory. The (preprocess (pps ppx_jane))
stuff is so we can use some syntax extensions that make dealing with the Incremental monad a bit easier. We’re also going to make a few changes to the default standard library, we’ll be using core_kernel
. To do this add a file called dune
in the root of the project with the following contents:
(env
(dev
(flags (:standard -warn-error -A -open Core_kernel)))
(release
(flags (:standard -open Core_kernel))))
This makes it so we use the core_kernel
standard library by default in all files we make. It also prevents warnings from failing the build (although this behavior may have changed recently). Now we’re ready to start making our application! In the root of the project execute dune build bin/main.exe
to make sure everything we’ve done so far works.
Getting Started
The Incr_dom architecture is loosely inspired by Elm and React. This means that applications are split into three main parts. The Model
, the Actions
, and the View
. A standard MVC-ish pattern. The Model
is 100% immutable, and the application can only use Actions
to effect change in the Model
’s data. There’s a special State
sub-module that can be used for stateful data, but we shouldn’t need that just yet. Let’s get started!
Create a new file in the lib
folder called tetris.ml
with the following contents:
open Incr_dom
module Model = struct
type t = unit
[@@deriving sexp, compare]
let init = ()
let cutoff t1 t2 =
compare t1 t2 = 0
end
module Action = struct
type t = unit
[@@deriving sexp]
end
module State = struct
type t = unit
end
let on_startup ~schedule_action:_ _ =
Async_kernel.return ()
let apply_action (model : Model.t) (action : Action.t) _ ~schedule_action =
model
;;
let view model ~inject =
let open Incr.Let_syntax in
let%map model = model in
Vdom.Node.(div [] [text "Hello World!"])
;;
let create model ~old_model:_ ~inject =
let open Incr.Let_syntax in
let%map apply_action =
let%map model = model in
apply_action model
and view = view model ~inject
and model = model in
Component.create ~apply_action model view
;;
There’s a decent amount of boilerplate to go over here. The [@@deriving sexp, compare]
are the syntax extensions talked about earlier. It’s an attribute that does some codegen by those preprocessors so we don’t have to write the comparison or serialization to S-expressions. Similarly the let%map
code is another syntax extension that makes working with monads easier. More can be learned here.
Starting from the top, the Model
module has a basic model of type t = unit
for now to make the compiler happy. It also has an init
variable representing the initial state. The cutoff
function is what Incr_dom uses to determine if the model has changed. This result determines if the incremental computations need to be recomputed.
In the Actions
module there’s not much happening. We make a default type t = unit
like the Model
to make the compiler happy. The [@@deriving sexp]
is because the serialization functions are required by the interface.
We won’t be using the State
module and it will stay as this simple implementation. It’s only here because it’s also required by the Incr_dom application interface.
The on_startup
function gives an opportunity to setup state and asynchronous functions. It’s useful for kicking off refresh actions or timers when the application loads. We won’t need it for the moment.
Next is apply_action
which takes an Action.t
, updates the model, and then returns a new model. Since we have no actions or model at the moment, it returns the passed in model.
The view
function is what generates the virtual-dom of our application. It takes in a Model.t Incr.t
which is the model lifted into the Incremental monad. Since we need to return a Vdom.Node.t Incr.t
we need to map
the Model.t Incr.t
to Vdom.Node.t
hence the let%map model = model in
code. This makes it so the value of the let
body is the mapped value and handles lifting it back up into an Incr.t
. Really it’s calling Incr.map
and putting all the code in the right places. It only makes things look nicer and avoid the endless nested >>= >>|
operators. For example we could have written the code in the following ways:
let view model ~inject =
Incr.map model ~f:(fun model ->
Vdom.Node.div [] []
)
let view model ~inject =
let open Incr in
model >>| fun model ->
Vdom.Node.div [] []
Which doesn’t seem too bad right now, but gets much uglier and messier when we start projecting properties out of the model to be incrementalized. So we’ll stick with the let%map
for now as it’s the one we want in the end.
Lastly, we have the complicated create
function. This ultimately calls the Component.create
function which bundles up our state into something the Incr_dom framework can use. The complication of this method stems from flexibility. The reasoning is here (see ‘Streamlining Incr_dom’). To summarize, it lets you share and incrementalize your application state more flexibly between the Model
, Action
, and view
. In our case we don’t really need much sharing, so we do the minimum to split things up.
There’s two more steps before we have a baseline application up and running. We need to edit the bin/main.ml
file to initialize the Incr_dom framework with our module, and create our index.html
file. Open bin/main.ml
and change its contents to:
open Incr_dom
let () =
Start_app.start
(module Incr_dom_tetris.Tetris)
~bind_to_element_with_id:"app"
~initial_model:(Incr_dom_tetris.Tetris.Model.init)
It’s pretty straightforward. We want to create an app that uses our Tetris
module and attaches it to the app
element on our webpage. Now we need to create that index.html
. Create index.html
in the root of the project with the following contents:
<!DOCTYPE>
<html lang="en">
<head>
<meta charset="utf-8">
<script src='_build/default/bin/main.bc.js' type='text/javascript'></script>
</head>
<body>
<div id='app'></div>
</body>
</html>
Now all that’s left is to build the project! Run dune build bin/main.bc.js
. This will compile the javascript version of our code. You can also compile the native version using dune build bin/main.exe
which is not super useful because it won’t have the javascript environment. But it is useful if we use a different bin
module, for example our test executable can be native and call into our Tetris
library for testing.
After building you should be able to open index.html
and see Hello World!
on the screen. If you open the console you’ll see something like:
Incr_dom action logging is disabled by default.
To start logging actions, type startLogging()
To stop logging actions, type stopLogging()
Which is super helpful later when debugging the application, as it will print all the intermediate state changes as they happen to the console. Congrats! You have the start of an Incr_dom project. It may seem like a decent amount of work here, but this is something that can easily be turned into a template so you never have to run through it again. But it’s good to go through at least once so there’s a base understanding of the different moving parts.
In case things have gone awry the code has been pushed here (on branch part1
).
Ending Remarks
In this installment we managed to get a basic Incr_dom application going. It doesn’t do much at the moment, but that’ll happen soon enough. A lot of new libraries and tools were introduced in this post, so some extra links have been included below.
Extra Reading
Incr_dom
Documentation
Tutorials + Examples
Tech Talk
Blog Post
Incremental
Documentation
Announcement Blog Post
History Tech Talk
Incremental and UI Tech Talk
Overview Tech Talk