ReasonML ↗ is new syntax and toolchain for working with Ocaml, supported by Facebook. It promises type-safe performant code with near-zero overhead.
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.
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.
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:
unit
is "nothing", and (unit => unit)
means function from nothing to nothing, this is used for callback functions[@@bs...]
are Bucklescript annotations for FFI ↗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:
Random.float
functions from Reason core)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.
I've added a simple collision detection to the first project and got laplacian growth out of it:
Basic algorithm is:
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:
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.
1423 words published on 2017-05-31 — let me know what you think