The ultimate answer to Belt vs Js in ReScript

I’ve finished the first version of the article The ultimate answer to Belt vs Js in ReScript. It describes a solution that exists only in my head, but I’ve decided to write the article first to get your feedback on the idea and whether you are interested in applying it.
Looking forward to hear your thoughts.

8 Likes

quick take but I dont know if theres a reason to be so prescriptive about it.
This is the KXCD N+1 standards problem after all.

I’ve taken to having our own Array, Option modules and prefer to pass calls through those and dispatch to Belt/Js as desired but I’m pretty loose about leaving Belt/Js code in components otherwise.

A good example this morning I added a ~desc field to Option.getExn so we can know which exception got hit and it took about…20 min all told.

A

2 Likes

It’s more about how you make sure that nobody uses Belt.Option.getExn in some other place.

Very cool! Thanks for sharing :slight_smile:

I couldn’t get rescript-stdlib-cli, is it an npm library you published? Is the source-code somewhere public?

Out of curiosity do you know if there is a way to open this module globally? For modules installed from npm you can use "-open Belt", in your bsconfig file, but I’m not sure about user defined libraries.

The article was written before I’ve created the actual tool, to get a feedback from both my team members and community. It was good enough so I’ve started working on the actual thing. You can find it in the repo https://github.com/DZakh/rescript-stdlib-cli, probably will take a few weeks to properly finish it. I’ll write in more details when it happens.

You can, but it should be in a separate package (bs-dependency), or you’ll get the dependencies cycle error.

3 Likes

I finished a working prototype but came across a problem that the compiler adds Curry for reexported functions.
For example, if we have:

// Stdlib.res
module Option = {
  include Belt.Option
}
// Demo.res
Js.log(Some("Hello world!")->Stdlib.Option.isSome)

The output will be:

// Demo.bs.js
console.log(Curry._1(Stdlib.$$Option.isSome, "Hello world!"));

The same when we have:

// Stdlib.res
module Option = {
  let isSome = Belt.Option.isSome
}

But it successfully inlines the Curry with:

// Stdlib.res
module Option = Belt.Option

Or:

// Stdlib.res
module Option = {
  // Note, it won't create an additional function and compile to: var isSome = Belt_Option.isSome;
  let isSome = option => Belt.Option.isSome(option)
}

It’s actually a problem because the solution intended the reuse of existing Belt and Js modules and the ability to extend them with project-specific functions. But by doing it this way, we’ll get every function wrapped in a Curry.

Now I’m thinking about whether I should write a script that will run over generated JS files and clean up these Curry functions, but I don’t want to make things even more complicated for the end user. Also, I consider going to the compiler repo and figuring out why it adds Curry. I have a guess, but still, feel scared in front of OCaml code :sweat_smile:

Do you have some suggestions?

UPD: On my machine Curry._1 runs from 47_229_154 to 84_593_566 times per second depending on the number of arguments. So might not be worthy of warring about.

2 Likes

Would you mind putting your guess? I’ve looked at the compiler code a few times, but am still not very familiar with it.


If your goal is a custom stdlib that can include Js and Belt modules, plus extend them with your own functions, and remove the currying, you will need to avoid nesting the modules.

Rather than having your Stdlib.res look like this

module Option = 
  include Belt.Option

  // other functions here
}

// other modules here

Rather do this:

module Option = Option
module Result = Result
// ... etc ...

Then have Option.res/resi, Whatever.res/resi, looking somthing like this:

Option.res

include Belt.Option

// Private functions.
let silly_private_helper = x => Js.log(x)

// Public extension functions.
let foo = t =>
  switch t {
  | Some(_) as x => {
      silly_private_helper(x)
      x
    }

  | None as x => x
  }

let bar = t => t
let baz = t => t

Option.resi

include module type of {
  include Belt.Option
}

// Technically Belt.Option doesn't have types of its own, so you could do
//
//   include module type of Belt.Option
//
// as well.  Doesn't matter in this case.

let foo: option<'a> => option<'a>
let bar: option<'a> => option<'a>
let baz: option<'a> => option<'a>

// More of your "extension" function signatures below...

If you use those functions in another module, you can check it and see that they don’t have the extra curry indirection.

Of course…one downside to this is that you have direct access to the Option and other modules that really should (probably) be restricted to being accessed through your Stdlib module. You could fiddle around with naming scheme but it still doesn’t change the visibility thing. Maybe that’s not a problem, but it is a little yuck, imo.

Alternatively, you can restrict the visibility of the “inner” modules by making your stdlib into a separate package and using the bsconfig namespace option. Check it out.

Your stdlib as a package

Edit: I didn’t try it, but maybe pinned dependencies would better suite your needs.

Let’s assume now that you want to make a separate library/package for your company stdlib. Let’s just say you call it AcmeBase.

In that package bsconfig.json you will have these lines in there somewhere.

  "name": "acme-base",
  "namespace": true,

The namespace part will wrap up your packages modules in a AcmeBase module (see here).

Then, your acme-base source dir just looks like this.

src/
    - Option.res
    - Option.resi
    ... others ...

Note there isn’t a need for an explicit AcmeBase.res file to coordinate your modules in this setup as the namespace thing takes care of it for you.

Assume the same contents for Option.res/resi files as shown above.

Before consuming this library from another app/package, first “install” it with npm link. (Or install it to npm, whatever you need.)

Your app consuming your stdlib

Okay, so now you start an app that wants to consume the AcmeBase lib. Just do this (assuming you went the link route…like say if you’re developing your stdlib in tandem with your app).

npm link acme-base --save
npm install

And to your bsconfig.json add: "bs-dependencies": ["acme-base"], so that your res files can access it too.

Btw, if you change stuff in the other acme-base package, like adding functions or whatever, those changes will automatically show up here. Sometimes, it required a restart of the language server, or running npm run res:clean every now and then, but still nice.

Now, here’s an example consumer file.

Demo.res

open AcmeBase

Some("Yooo")->Option.getExn->Js.log
Some("Yooo")->Option.isSome->Js.log
Some("Yooo")->Option.foo->Js.log
Some("Yooo")->Option.bar->Js.log
Some("Yooo")->Option.baz->Js.log

And you get this JS output (Demo.bs.js):

// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';

var Option$AcmeBase = require("acme-base/src/Option.bs.js");

console.log(Option$AcmeBase.getExn("Yooo"));

console.log(Option$AcmeBase.isSome("Yooo"));

console.log(Option$AcmeBase.foo("Yooo"));

console.log(Option$AcmeBase.bar("Yooo"));

/*  Not a pure module */

Nice and clean.

Summary

To summarize:

  • you get your custom stdlib, that can include Belt etc
  • it can also be extended with your own stuff
  • it is guarded behind module signatures (resi) to make the sig explicit and keep private stuff out,
  • it compiles to clean JS
  • it is behind a single namespace/module AcmeBase, so you can open it and know what’s going on (or use fully qualified names if that’s your preference)

Btw, here is the generated code for the Option module. You can see it isn’t sticking the included function in an object

Option.bs.js file
// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';

var Belt_Option = require("rescript/lib/js/belt_Option.js");

function foo(t) {
  if (t !== undefined) {
    console.log(t);
    return t;
  } else {
    return t;
  }
}

function bar(t) {
  return t;
}

function baz(t) {
  return t;
}

var keepU = Belt_Option.keepU;

var keep = Belt_Option.keep;

var forEachU = Belt_Option.forEachU;

var forEach = Belt_Option.forEach;

var getExn = Belt_Option.getExn;

var mapWithDefaultU = Belt_Option.mapWithDefaultU;

var mapWithDefault = Belt_Option.mapWithDefault;

var mapU = Belt_Option.mapU;

var map = Belt_Option.map;

var flatMapU = Belt_Option.flatMapU;

var flatMap = Belt_Option.flatMap;

var getWithDefault = Belt_Option.getWithDefault;

var orElse = Belt_Option.orElse;

var isSome = Belt_Option.isSome;

var isNone = Belt_Option.isNone;

var eqU = Belt_Option.eqU;

var eq = Belt_Option.eq;

var cmpU = Belt_Option.cmpU;

var cmp = Belt_Option.cmp;

exports.keepU = keepU;
exports.keep = keep;
exports.forEachU = forEachU;
exports.forEach = forEach;
exports.getExn = getExn;
exports.mapWithDefaultU = mapWithDefaultU;
exports.mapWithDefault = mapWithDefault;
exports.mapU = mapU;
exports.map = map;
exports.flatMapU = flatMapU;
exports.flatMap = flatMap;
exports.getWithDefault = getWithDefault;
exports.orElse = orElse;
exports.isSome = isSome;
exports.isNone = isNone;
exports.eqU = eqU;
exports.eq = eq;
exports.cmpU = cmpU;
exports.cmp = cmp;
exports.foo = foo;
exports.bar = bar;
exports.baz = baz;
/* No side effect */

Edit: cool blog post btw!

6 Likes

Thanks, it really does work. But now I have absolutely no idea why it’s not the case with nested modules.

1 Like

I finished the first version of the script. It can be installed with npm, although not recommended for production usage yet.

I have two public repositories where I applied the script, and you can take a look at how it works:

There are many things to improve, but before continuing to work on the lint logic improvements, I want to add a script to automatically migrate an entire codebase to the custom standard library. This will reduce the entrance threshold, allow better feedback and properly test on a more extensive codebase.

5 Likes

Published rescript-stdlib-vendorer@1.0.0 :rocket:

I’ve been using it in my personal projects for a few months and recently finished applying the solution to all Carla.se applications. I’d say it’s a must-have for any long-living project.

Here’s the finished article describing it in detail:

6 Likes