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.
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
It’s more about how you make sure that nobody uses Belt.Option.getExn
in some other place.
Very cool! Thanks for sharing
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.
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
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.
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!
Thanks, it really does work. But now I have absolutely no idea why it’s not the case with nested modules.
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.
Published rescript-stdlib-vendorer@1.0.0
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: