Szymon Kaliski

  1. Main
  2. Projects
  3. Notes
  4. Music
  5. Bio

Exploring ReasonML

ReasonML ↗ is new syntax and toolchain for working with Ocaml, supported by Facebook. It promises type-safe performant code with near-zero overhead.

Intro

After four months spent working on different tools I wanted a break, and explore a bit more.

ReasonML was on my to-learn list for quite a while, and while a month is not a lot to really learn something, I feel like I at least scratched the surface, and have better understanding and "feel" of this language.

I prefer to learn through doing, and I wanted to explore some generative design ideas I was thinking about. I've spent much more time playing with ReasonML than working on aesthetics, so I hope the presented code might be used as a starting point for doing something more interesting.

Project Setup

I followed original setup guide ↗ for getting my toolchain up and running.

Setting up nactual project is very easy, only bs-platform from npm is needed as dependency, and can be started then using local npm binaries: ./node_modules/.bin/bsb. I've set up simple npm script for building and watching for changes, using package.json scripts section:

{
  ...
  "scripts": {
    "build": "bsb",
    "watch": "bsb -w"
  }
}

Another thing that's needed is bsconfig.json in root directory of the project:

{
  "name": "project-name",
  "sources": "src"
}

./src/ folder is source folder where ReasonML code wil be kept, and running npm run build compiles it to ./lib/js/ folder.

This gives us a nice JavaScript code in ./lib/js, but we still need to be able to run it. For small projects I like using budo ↗ which also runs browserify so the output code from ReasonML can use npm modules as well!

My final package.json looked like this:

{
  "scripts": {
    "build": "bsb",
    "watch": "bsb -w",
    "serve": "budo ./lib/js/src/app.js --port 3000 --live",
    "start": "concurrently --raw 'npm run watch' 'npm run serve'"
  },
  "devDependencies": {
    "bs-platform": "^1.7.3",
    "budo": "^10.0.3",
    "concurrently": "^3.4.0"
  }
}

This allows me to start serving (and live-reloading) compiled JavaScript using npm run serve, and start both ReasonML compilation and budo with npm start. Bucklescript's ↗ compilation (the thing that compiles ReasonML to JavaScript) is extremely fast, and together with budo's live-reload gives near-instantaneous feedback that feels amazing.

It's much faster than typescript or flow compilation, and actually one of the fastest compilers I've ever used.

First project: flow-field

I've started exploring ReasonML through binding to canvas and some other DOM elements. Unfortunately, the official docs are lacking here, I tried to make sense of Bucklescript docs ↗ but they are written for Ocaml, not Reason, so that was tricky at first. What helped a lot was browsing through existing ReasonML projects:

I ended up writing just the bindings I've needed, first for Document:

let module Document = {
  type element;

  let window: element = [%bs.raw "window"];

  external createElement : string => element = "document.createElement" [@@bs.val];
  external appendChild : element => element = "document.body.appendChild" [@@bs.val];

  external addEventListener :
    element =>
    string =>
    (unit => unit) =>
    unit = "addEventListener" [@@bs.send];

  external getWidth : element => int = "innerWidth" [@@bs.get];
  external getHeight : element => int = "innerHeight" [@@bs.get];

  external setWidth : element => int => unit = "width" [@@bs.set];
  external setHeight : element => int => unit = "height" [@@bs.set];

  external requestAnimationFrame : (unit => unit) => unit = "requestAnimationFrame" [@@bs.val];
};

Few notes:

This allowed me to create canvas and set its size:

let canvas = Document.createElement "canvas";

Document.appendChild canvas;

let setCanvasSize () => {
  let width = (Document.getWidth Document.window);
  let height = (Document.getHeight Document.window);

  Document.setWidth canvas width;
  Document.setHeight canvas height;
};

Document.addEventListener Document.window "resize" setCanvasSize;
Document.addEventListener Document.window "DOMContentLoaded" setCanvasSize;

I've made similar simple bindings for canvas: src/Canvas.re ↗. Since every file is a module in Reason, we can drop let module Canvas = if saving code in separate file.

Flow-field app logic was quite simple to write, the basic algorithm is:

  1. create bunch of points ↗ (notice nice Random.float functions from Reason core)
  2. update point's velocity based on flow field direction in its current x/y position ↗

For flow-field direction, I use Simplex noise algorithm which generates one value based on two inputs, similarly to Perlin noise. I calculate vector from this value, treating it as angle:

let angleToVec angle : vecT => {
  x: (sin (angle *. 2. *. Math.pi)),
  y: (cos (angle *. 2. *. Math.pi))
};

Notice the vecT annotation, this allows compiler to be sure that returned value is { x: float, y: float }.

I've spent most time building this example exploring ReasonML syntax, and fighting the compiler.

Coming from working mostly in non-typed languages (I've been using JavaScript and Clojure lately), it felt like doing more work for little to no profit.

This was noticeably slowing me down, to the point that I probably won't ever use ReasonML for experimental/exploratory projects. At the same time, I'm sure that type safety would be great for any bigger UI application.

Second project: metaballs with regl

I've added a simple collision detection to the first project and got laplacian growth out of it:

Basic algorithm is:

  1. start with a fixed particle, and a lot of "floating" particles
  2. if floating particle hits the fixed particle, it becomes fixed as well
  3. repeat until you run out of particles

I got curious how this would look if the particles were metaballs ↗ — I was looking for more organic "blobby" visuals.

To create metaballs in 2d, we have to calculate distance to all the particles for each pixel, this would perform very badly in CPU-land, so I've decided to switch to webgl. Regl ↗ was another cool project on my to-learn list, so I decided to combine the two — learn about regl API while binding it to ReasonML, and then use it for metaballs.

I've spent a lot of time on writing a nice, usable wrapper. Regl is great to use in JavaScript, but I had a lot of problems with forcing Bucklescript and ReasonML types to cooperate. Final wrapper is very hacky, and I'm both ashamed and proud of it, some interesting parts:

let create : Document.element => unit = [%bs.raw {|
  function(canvas) {
    // first hacky thing, expose regl as top-level window things
    window.regl = require('regl')(canvas);
  }
|}];

This exposes regl as top-level window thing, so I can then use my wrapper as a singleton. Not sure if that's good or bad idea, but has worked for me.

external makeDrawConfig :
  frag::string? =>
  vert::string? =>
  uniforms::Js.t{..}? =>
  attributes::Js.t{..}? =>
  count::int? =>
  drawConfigT = "" [@@bs.obj];

let makeDrawCommand : drawConfigT => drawComandT = [%bs.raw {|
  function(opts) {
    var command = window.regl(opts);

    /* expose ".draw" so we can use draw external */
    command.draw = command;

    return command;
  }
|}];

external draw : command::drawComandT => uniforms::Js.t 'a? => unit = "" [@@bs.send];

This allowed me to create JavaScript object using ReasonML syntax:

let drawTriangleConfig = Regl.makeDrawConfig
  frag::triangleFrag
  vert::triangleVert
  count:4
  ...

Then use this configuration object to create hacked draw command:

let drawTriangleCommand = Regl.makeDrawCommand drawTriangleConfig;

Which finally can be used in Regl.frame to draw:

let draw t => {
  Regl.clear
    color::[| 0, 0, 0, 1 |]
    depth::1;

  let triangleUniforms = [%bs.obj {
    width: state.window.width,
    height: state.window.height,
  }];

  Regl.draw
    command::drawTriangleComand
    uniforms::triangleUniforms;
};

Regl.frame draw;

Full wrapper is here: src/Regl.re ↗, and final results below:

Fin.

I enjoyed this month's exploration and learning a new language as nice break from tool-making research.

ReasonML is an interesting project, I love how fast the compiler is, how readable the output code is, and how types could help while working with data structures and complex applications.

It still feels like early days though, with not the greatest documentation, and only few example projects/libraries using it. Right now, I would consider using it for UI frontend work, and I'll keep my eye on its future developments.

If someone is interested in making the regl wrapper better — feel free to contact me. I'd love to help, but I probably won't be pursuing this myself any time soon.

Backlinks

  1. 2017-10-02Learning Haskell1

1423 words published on 2017-05-31let me know what you think