Curious about JS output for different ways to define a module

I was playing around with some toy code and I got some (seemingly) interesting differences in JS output. The question I have is why the examples which seem to have pretty similar ReScript code all get pretty different JS output. (Examples follow.)

Simple way

If you have code like this:

type document
@val external document: document = ""
@send external getElementById: (document, string) => Dom.element = "getElementById"

let apple = getElementById(document, "apple")

Then you get nice compact JS output:

var apple = document.getElementById("apple");

Putting the code in a simple module like this:

module Docu = {
  type t
  @val external document: t = ""
  @send external getElementById: (t, string) => Dom.element = "getElementById"
}

also gives you the same JS.

Module split into .res and .resi

If I split the module into a .res file:

type t

@val external document: t = ""
@send external getElementById: (t, string) => Dom.element = "getElementById"

And a .resi file:

type t

let document: t
let getElementById: (t, string) => Dom.element

Then my JS looks like this:

'use strict';

var $$document = document;

function getElementById(prim, prim$1) {
return prim.getElementById(prim$1);
}

exports.$$document = $$document;
exports.getElementById = getElementById;

And when I actually use that code from another module (eg different file…whatever)

let apple = DocuThing.getElementById(DocuThing.document, "apple")

Then I get this:

var DocuThing = require("./DocuThing.bs.js");

var apple = DocuThing.getElementById(DocuThing.$$document, "apple");

Now, the ReScript code looks pretty much the same as the simple module except that you have a signature to go with it and it changes the JS output. Let’s check one more example.

Weirder Example

Here are a couple of different module definitions that give yet different JS output.

module Docu2: {
  type t
  let document: t
  let getElementById: (t, string) => Dom.element
} = {
  type t
  @val external document: t = ""
  @send external getElementById: (t, string) => Dom.element = "getElementById"
}

let good = Docu2.getElementById(Docu2.document, "good")

module type Docu3 = {
  type t
  let document: t
  let getElementById: (t, string) => Dom.element
}

module Docu3: Docu3 = {
  type t
  @val external document: t = ""
  @send external getElementById: (t, string) => Dom.element = "getElementById"
}

let sweet = Docu3.getElementById(Docu3.document, "sweet")

And the generated output looks something like this:

function Docu2_getElementById(prim, prim$1) {
  return prim.getElementById(prim$1);
}

var Docu2 = {
  $$document: document,
  getElementById: Docu2_getElementById
};

var good = Curry._2(Docu2_getElementById, document, "good");

function Docu3_getElementById(prim, prim$1) {
  return prim.getElementById(prim$1);
}

var Docu3 = {
  $$document: document,
  getElementById: Docu3_getElementById
};

var sweet = Curry._2(Docu3_getElementById, document, "sweet");

That gives another, different JS output…this time with the explicit JS currying methods.

Wrap up

Okay, so that was long winded, but I’m pretty curious about what leads to the different JS output. If anyone could shed some light on the matter that would be great!!

When you make the interface abstract enough, the compiler can’t ‘see’ that getElementById is just a binding, it thinks it’s an actual allocated function of that type. So it generates that function.

If you ‘reveal’ more information in the interface, then the compiler can ‘see’ that it’s just a binding. In other words, copy the same bindings into the .resi file:

type t

@val external document: t = ""
@send external getElementById: (t, string) => Dom.element = "getElementById"

This gives the compiler enough information to actually generate zero-runtime code using the bindings.

If you’re wondering why you need the redundancy, you probably don’t–there’s no need for an interface file if the file just contains types and bindings. Unless of course you want to hide some of the bindings, or hide some type definitions.

7 Likes

Thanks, that makes a lot of sense!

1 Like