Binding to a global object with @scope v/s as a JS Object

Is there any difference between

module Math = {
  @val @scope("Math") external pi : float = "PI" 
}
Js.Console.log(Math.pi)

// JS output:
// var $$Math = {};
// console.log(Math.PI);
// exports.$$Math = $$Math;
/*  Not a pure module */

and

type math = {
  @as("PI") pi: float
}
@val external math: math = "Math"
Js.Console.log(math.pi)

// JS output:
// console.log(Math.PI);

besides that the first approach produces noise in JS output, while the second approach is a bit verbose?

There is a third approach (a cleaner alternative to approach 1):

@val @scope("Math") external pi : float = "PI" 
Js.Console.log(pi)

// JS output:
// console.log(Math.PI);

which I am ignoring because I prefer that the first two approaches allows to namespace usages (Math.pi / math.pi).

Which of these would you prefer if the bindings were not limited to just one property/function of the object being bound?

I thought of mapping the usage of @scope to global singleton objects (as recommended in official docs too) like Math, window, window.location, etc. But this could become tedious and even errorprone (e.g., @scope(("window", "location"))) for a lot of bindings.

1 Like

One problem I discovered in approach 2 is currying. I bound Math.floor() thus:

type math = {
  @as("PI") pi: float
  floor: float => int
}
@val external math: math = "Math"
Js.Console.log(math.floor(1.))

// output:
// var Curry = require("./stdlib/curry.js");
// console.log(Curry._1(Math.floor, 1));

After applying explicit uncurrying:

type math = {
  @as("PI") pi: float
  floor: (. float) => int
}
@val external math: math = "Math"
Js.Console.log(math.floor(. 1.))

// Output:
// console.log(Math.floor(1));

I don’t know why explicity uncurrying had to be applied to this case, but this pretty much tips the balance in favour of approach 1.

1 Like

I think minting a new module for your bindings is more convenient. I’m guessing the math module may be just an example to illustrate your point, but sticking with it, I could definitely imagine wanting to use those bound math function from functions in other modules/files.

Imagine your bindings live in the Math.res file. In that case, I would rather write Math.floor(8.8) rather than Math.math.floor(8.8).

E.g.

Math.res

@val @scope("Math") external pi: float = "PI"
@val @scope("Math") external floor: float => int = "floor"

type math = {
  @as("PI") pi: float,
  floor: (. float) => int,
}
@val external math: math = "Math"

Example.res

let x = Math.floor(8.8)
let y = Math.math.floor(8.8)

That’s an excellent point. This and the currying issue blows the approach 2 out of the water.

1 Like