Eccentric Developments


Raybench - ReScript

Well, this is not what I was planning to write for this entry but here we are.

Last time I went through a huge round of programming languages to implement raybench, I stumbled upon OCaml. Ocaml is a ML derived language that has an excelent type system and I found it to be a great experience.

Some time passed and I learned about a derived language that took the OCaml ecosystem and compiled into Javascript; this language is ReScript.

ReScript has had an interesting story and more details on that you can read about HERE.

Anyway, as I was saying, I was not planning to do another implementation of raybench, but when I started experimenting with ReScript, I just couldn't stop.

ReScript has the great type system you find in OCaml, but compiles to readable Javascript and can interact with the Javascript libraries and APIs we all know, not that I use any of those in raybench, but they are there.

The type system is very strict, enforcing the types of the variables and helping you inferring the types along the way, if you are feeling lazy. This is in contrast to the other language I've used a lot lately, Typescript, which you can easily escape the type system and run havoc on your data (I'm looking at you any).

Javascript backend, must be slow, right?!

ReScript also brings a code optimizer that helps a lot, just because of this, the implementation of raybench got a very impressive speed boost, look at this table:

| Language   | Running Time | Version                                     |
| ---------- | ------------ | ------------------------------------------- |
| Rust Alt   | 14.9163 (s)  | rustc 1.70.0 (90c541806 2023-05-31)         |
| C          | 16.2371 (s)  | gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0 |
| ReScript   | 65.4431 (s)  | 10.1.4                                      |
| Javascript | 120.0792 (s) | v18.16.0                                    |

Now here is the interesting part, ReScript is compiled to Javascript, then run in node (v18.16.0), and manages to beat the vanilla Javascript implementation finishing in almost HALF THE TIME!. This is a result of many things, for example there is no use of classes, the optimizer is very agressive, and shows more importantly, that I'm a bad Javascript developer.

How does the language looks?

Given those awesome results, ReScript also is a very expressive language, look at the following snippet of the raybench test:

let rec trace = (world, ray, depth): vector =>
  switch depth {
  | n if n === max_depth => zero
  | _ => {
      let rayhit = sphit(_, ray)
      let closestHit =
        world.spheres
        ->Belt.Array.map(rayhit)
        ->Belt.Array.reduce(None, (closest, hit) =>
          switch (closest, hit) {
          | (_, None) => closest
          | (Some({distance: d1}), Some({distance: d2})) if d1 < d2 => closest
          | (_, _) => hit
          }
        )
      switch closestHit {
      | None => zero
      | Some({sphere: {islight: true, color}}) => color
      | Some({point, normal, sphere: {color}}) => {
          let nray = {origin: point, direction: rnddome(normal)}
          let ncolor = trace(world, nray, depth + 1)
          let at = dot(nray.direction, normal)
          vmul(color, vmuls(ncolor, at))
        }
      }
    }
  }

This is quite a huge thing to unpack but it the gist is:

  • The rec keyword indicates the function can called recursively.
  • The switch statement enables pattern matching, the values passed to it are tested against the given branches and executes the corresponding one.
  • The _ in sphit(_,hit) creates a curried monadic function, and the parameter passed to it will sent to _.
  • Pipelinening is supported by -> which means the result (or value) on the left, is injected to the first parameter in the function to the right, and get executed.
  • Belt.Array.map and Belt.Array.reduce are the classic map/reduce functions that work on arrays.

What about Javascript?

In comparison, this is the same function but in plain Javascript:

function trace(randf, world, ray, depth) {
  let color = new Vector3();
  let did_hit = false;
  let hit = new Hit(1e15);
  let sp;

  world.spheres.forEach((s) => {
    let lh = s.hit(ray);

    if (lh && lh.dist < hit.dist) {
      sp = s;
      did_hit = true;
      color = s.color;
      hit = lh;
    }
  });

  if (did_hit && depth < MAX_DEPTH) {
    if (sp.is_light != true) {
      let nray = new Ray(hit.point, rnd_dome(randf, hit.normal));

      let ncolor = trace(randf, world, nray, depth + 1);
      let at = nray.direction.dot(hit.normal);

      color = color.mulv(ncolor.muls(at));
    }
  }

  if (did_hit == false || depth >= MAX_DEPTH) {
    color = new Vector3();
  }

  return color;
}

I find this not as clean and easy to follow as the ReScript version, since it has more assigments and brances. I think I'll be looking for any oportunity to use ReScript in my projects from now on.

If you want to try ReScrit go to their website, and if you want to see more of the raybench inplementation, go to this github repository.