Running OCaml in your Browser
Last Updated: 23, September 2020 at 13:48:05
Platform tools: Dune
Overview
OCaml byte-code can be compiled to Javascript to run in your browser using the js_of_ocaml
compiler. This allows you to write statically type-checked OCaml code to run in a browser, rather than dynamically typed Javascript leading to safer code at runtime.
This workflow introduces the key concepts to writing OCaml for the browser, manipulating JSON and programming in an event-driven way which is common for web applications.
Recommended Workflow
Cross compiling to Javascript
Dune has built-in support for compiling byte-code to Javascript. Consider a "hello-world" example:
let () = print_endline "Hello World"
This can be built with dune build
, provided the js_of_ocaml
package is installed (to get the cross-compiler) and the following dune file:
(executable
(name main)
(modes js))
This will output your main file into a Javascript file called main.bc.js
. To get this running in your browser you can then create an index.html
file in the same _build/default
folder and include the JS file in a script tag: <script src="main.bc.js></script>
. This is the quickest way to get started. If you had node installed you could also run it using node _build/default/main.bc.js
.
Interoperating with Javascript
Although Javascript has many features in common with functional-styled programming, it primarily works using objects and mutable data. To make the transition from OCaml to Javascript easier, there is a ppx to make objects using the OCaml syntax and access methods and properties. There is a separate workflow if you are unfamiliar with OCaml's meta-programming capabilities with ppx. Once installed, you can make objects fairly simply. Make sure you preprocess your files by adding (preprocess (pps js_of_ocaml-ppx))
to your dune file.
(executable
(name main)
(preprocess
(pps js_of_ocaml-ppx))
(modes js))
let person =
object%js (self)
val name = "Alice" [@@readwrite]
method get = self##.name
method set str = self##.name := str
end
let () =
print_endline person##get;
person##set "Bob";
print_endline person##get
Here we use the ppx in conjunction with OCaml's object system to create Javascript objects. By tagging properties (val name
) with attributes you can change the semantics of how they are used. With [@@readwrite]
we force name
to be readable and writable.
A common desire is to call pre-existing Javascript functions, constructors and variables from OCaml. The recommended way to do this is to add as much type safety as you can. Consider this snippet of Javascript that we pretend is being provided by a library.
// A simple person object
class Person {
constructor(name) {
this.name = name
}
printName = () => {
console.log(this.name)
}
}
We want to (a) define a matching class type for this object, (b) construct a new object using the Person
constructor and (c) call the printName
function. First, the class type. We use OCaml's built-in object system to do this.
(* Defining the Person object type *)
class type person =
object
val name : Js.js_string Js.prop
method printName : unit -> unit Js.meth
end
In order to interact directly with Javascript we must provide the Js
types for methods and properties. Here name
is a Js.js_string
property and printName
takes a unit
and returns one. The next step is to bind the constructor.
let person : (Js.js_string Js.t -> person Js.t) Js.constr =
Js.Unsafe.js_expr "Person"
let () =
let v = new%js person (Js.string "Alice") in
v##printName ()
The person
function has type (Js.js_string Js.t -> person Js.t)
as we must provide a Javascript string in order to fulfill the original constructor and in return we get our person object. Js.t
is the type of Javascript objects. In order to call the constructor we use the ppx syntax with new%js <constructor> <arguments>
. Finally we use the ppx syntax again (##
) to call the printName
function.
Working with JSON
Javascript Object Notation (JSON) is a data format that is very commonly used for web applications and exchanging information over the internet. There is a very close relationship between OCaml types and JSON structures. The two main types being objects (similar to OCaml records) and arrays (similar to OCaml lists or arrays). There are a few OCaml libraries for working with JSON including jsonm (and the simplified ezjsonm) and yojson.
Yojson is perhaps the friendliest interface for working with JSON. There is also a ppx_deriving_yojson library which will automatically generate the encoding (to_yojson
) and decoding (of_yojson
) functions for your OCaml types. Here's an example:
type person = { name : string; age : int } [@@deriving yojson]
type db = person list [@@deriving yojson]
Which must be compiled with an appropriate dune file.
(executable
(name json)
(libraries core yojson)
(preprocess
(pps ppx_deriving_yojson)))
This produces the functions person_to_yojson
and person_of_yojson
to convert between types (similarly for db
). With a small main function we can read a JSON file converting the content to Yojson before converting to the underlying OCaml types. After this it converts those types back to Yojson and then prints them (of course you could then print these in JSON to a file).
let () =
let db_string = In_channel.read_all "db.json" in
let db = Yojson.Safe.from_string db_string in
match db_of_yojson db with
| Ok t -> Yojson.Safe.pp Format.std_formatter (db_to_yojson t)
| Error s -> failwith s
The JSON file.
[
{
"name": "Alice",
"age": 42
},
{
"name": "Bob",
"age": 24
}
]
And the output of the program.
$ dune exec ./json.exe
`List ([`Assoc ([("name", `String ("Alice")); ("age", `Int (42))]);
`Assoc ([("name", `String ("Bob")); ("age", `Int (24))])])
Event-driven programming
A lot of web development is based on events and more specifically user interaction with the HTML Document Object Model (DOM). Js_of_ocaml offers a solution to writing OCaml programs that interact with DOM and trigger events on user input.
One of the simplest programs is to log some message whenever a button is pressed. To do this we need to:
- Find the button in the HTML DOM.
- Add some function to the
onClick
handler that we want to be called whenever the button is clicked. - Register all of this once the window has loaded (otherwise the elements won't be there).
open Js_of_ocaml
open Lwt.Infix
module Events = Js_of_ocaml_lwt.Lwt_js_events
module Html = Dom_html
let add_handler id =
let btn = Html.getElementById id in
btn##.onclick :=
Html.handler (fun _ ->
print_endline "Clicked!";
Js._false)
After opening some important modules and creating shorter aliases to others, the first function we define is add_handler
. This takes an (HTML) element id and adds a simple function to the onClick
handler. This is built using the handler
function which returns a Js
value of true
or false
. Returning false
suppresses the default behaviour.
let rec key_listener key =
Events.keydown key >>= fun event ->
if event##.keyCode = 32 then Lwt.return (print_endline "Key Pressed!")
else key_listener key
The key_listener
example is a little more complex as it uses Lwt to provide asynchronous functions using promises. Here we are listening to the keydown
event and checking if the keyCode
matches the "space" key (32) and printing "Key Pressed!"
if it does. Otherwise we recursively wait for the next key down event.
let onload _ =
add_handler "button";
Js._false
let () =
Html.window##.onload := Html.handler onload;
Lwt.async (fun () -> key_listener Html.document)
The final part to our small JS program is to load everything we need. We add a handler that is called when the Html.window
loads. We also call the key_listener
function asynchronously.
Real World Examples
Whilst programming in pure js_of_ocaml
is very much a possibility, it is also nice to use frameworks which can help build more complex and performant web applications. One such framework is bonsai. It uses Jane Street's Incremental
and Incr_dom
library under the hood to make DOM re-rendering faster. They provide a good set of examples to get you started.
There is also Ocsigen's Eliom framework for building web and mobile applications. Their client-server example introduces a lot of the key ideas behind their framework.