This post is a result of a game I made in OCaml for js13k game jam. I will share some observations I had when creating the game.
But first let me briefly introduce js13k.
js13k is a coding competition for javascript developers. The goal is to create a game in one month that follows the given theme. The challenge is that the whole zip package has to be no more than 13kB in size. Everything that is required to run the game must be included in the package - the game cannot load any resources from the web and cannot access external services.
Since this was my first time using OCaml, I wanted to build something simple to make sure I was going to make it and wouldn't go beyond the required size. The theme for this year was "404 not found", so i created a guessing game in which the player needs to reveal a word or a phrase by guessing letters - similar to the game of hangman. This was pretty basic in itself, so I've added animations after each correctly guessed phrase. Each phrase has it's own animation and this turned out to take about 1/4 of the code size.
The submitted game must be implemented in javascript. You can write in any language, as long as it can be transpiled to javascript. I've submitted a pure javascript game in previous years, but this time I wanted to try a language that is compiled and has static typing.
My main issue with javascript is that it's dynamically and weakly typed. This can lead to weird errors that can only be discovered at runtime. It also makes any nontrivial refactoring much harder, because you now need to rerun the application to check if everything is still working. Lastly, static typing has additional advantage - it's a kind of automatic documentation of input and output.
Here's some weird results you can get in javascript:
'a' + 1 // 'a1'
'a' - 1 // NaN
'a' + NaN // 'aNaN'
[] == [] // false
[] == ![] // true
All of these are either highly surprising or don't make any real sense.
In contrast, OCaml is statically and strongly typed language. This results in constructs like "a" + 1
simply being a compilation error. It goes even further, because even adding an integer to a float doesn't compile - you need to cast both arguments to the same type. This simplifies development, because it yields no surprising behaviour. Refactoring is also much simpler, because compiler checks the correctness at least partially.
OCaml doesn't compile to javascript out of the box, but there's a library just for that: js_of_ocaml.
It plays well with OCaml tooling, you can install it using opam package manager with command
opam install js_of_ocaml js_of_ocaml-ppx
js_of_ocaml
is the base package js_of_ocaml-ppx
provides syntax extensions for OCaml code. There are a few more packages that provide additional functions.
Once installed, you first compile OCaml code to byte code as you'd normally do, for example with ocamlc
:
ocamlfind ocamlc -package js_of_ocaml -package js_of_ocaml-ppx -linkpkg -o script.byte script.ml
and then you invoke js_of_ocaml
to generate javascript from OCaml byte code
js_of_ocaml script.byte
dune, commonly used build tool for OCaml, also has direct support for compiling OCaml to javascript with js_of_ocaml
.
Functions provided by js_of_ocaml
are split to several modules:
module Js
- javascript bindings
module Dom_html
- DOM HTML bindings
module Firebug
- debugging console
and many other
All functions operate on types provided by js_of_ocaml
, which among other are:
Js.number
Js.date
Js.regex
Js.js_string
Js.opt
Js.optdef
number
, date
and regex
map to the types known from javascript. There's also js_string
, which is javascript's string type. It provides all methods that javascript string has and therefore is not compatible with OCaml string type. That's why it's often needed to do conversions between the two types.
opt
and optdef
are special types for handling null and undefined values: opt
represents a possibly null value, optdef
- possibly undefined value. As such, there's no notion of type that can be both null and undefined.
All types in js_of_ocaml
package are designed in an object oriented way, so you call methods and read/set properties on types. Type signatures of these types are somewhat complex, so to ease that you should use syntax extensions provided by js_of_ocaml-ppx
package. This gives you syntax that is pretty similar to normal object oriented OCaml code:
element##method_name arg1 arg2 (* method call *)
let foo = element##.property (* read value of a property *)
element##.property := new_value (* set new value for a property *)
For example, here I call querySelector
to get all elements that have class big
:
Html.document##querySelector (Js.string ".big")
Here's how I can get current color of some DOM element elem
:
let color = elem##.style##.color
And I can change current colour to, e.g. "black":
elem##.style##.color := Js.string "black"
I can also assign event handlers:
Html.document##.onkeydown := Html.handler keypressed
Here's some less trivial example - a function that adds a CSS class to a given element:
let jstr s = Js.string s
let nullstr () = jstr ""
let addClass (elem: Html.element Js.t) cls =
let class_str = (jstr "class") in
let current_cls = (elem##getAttribute class_str |> Js.Opt.get) nullstr in (* 1 *)
let new_cls = current_cls##concat_2 (jstr " ") (jstr cls) in (* 2 *)
elem##setAttribute class_str new_cls (* 3 *)
At (* 1 *)
I get current CSS classes assigned to the element. The getAttribute
method results in a Js.Opt
type, so it can be null. To get the actual value I pipe it to Js.Opt.get
function and it returns the stored value if it's there. Otherwise it returns the value returned by nullstr
function, which is empty string.
At (* 2 *)
I append the new class to the class list from (* 1 *)
.
At (* 3 *)
I finally set the newly constructed string as current for the element.
One thing to keep an eye on is the resulting package size. When all you have is 13kB, even bytes can count. Unfortunately, js_of_ocaml
adds about ~6kB of overhead upfront. That's quite a lot, especially since it doesn't provide any extra features to create a web based game. And that can grow even more if using some additional OCaml functions. When I tried to use Printf.printf
function, resulting package size increased by ~15kB. I had to fall back to simple string concatenation.
One more case when I had an issue with code size was when I had to compare OCaml string with Js.js_string
. To do that I needed to convert one of them so that both have the same type. When I converted OCaml string to Js.js_string
package size was bigger than when I converted Js.js_string
to Ocaml string. The difference was ~4kB, so was definitely noticeable.
To sum it up, it was quite fun to try to make a javascript game in OCaml. Having a strict type checker definitely helped. I had only one runtime error overall - and only because I did some silly typo on CSS class name. This wouldn't be possible in pure javascript. Iteration cycle was pretty quick, but obviously slower than in javascript, because there's no compilation step. Compilation time was quite quick, but noticeable.
The main issue is generated javascript code size, which can rise quite quickly in some situations if you're not careful. But if javascript code size doesn't matter - and it usually doesn't - js_of_ocaml
is viable way of writing javascript code.
Top comments (0)