Contents

Getting Started With Incr_dom - Part 1

Contents

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