19
From Reason/React to Rescript/React, Guaranteed Uncurrying
Before we get started, please do ping me if you need a ReasonML/Rescript dev on your team.
I love reading old ReasonML code. The devs who took the time write ReasonML projects in the early days really had to know what they were doing and it shows. Its an excellent resource for learning both ReasonML and OCaml. For me, the older the code, the better. When I came across this repo and saw it was four years old, I could not resist. On top of that, its a wonderful little piece of code.
Today's old repo is si-reason by @scottcheng who was dabbling in ReasonML 4 years ago when the compiler was on [email protected]
. Except for his React version, his code ran today on Rescript and ReasonML latest. You can see what he built at https://scottcheng.github.io/si-reason/.
As I'm studying this old reason code, I do some clean up, update to latest ReasonML and get it working. Then I decide to switch the syntax to Rescript since that is the new thing and I come across this one line that doesn't work after converting it from ReasonML to Rescript.
let (state, dispatch) = React.useReducer((_, action) =>
switch action {
| UpdateColumnPositions => {
columnPositions: emptyColumnPositions
|> Array.mapi((x, row) =>
row |> Array.mapi((y, _) => {
let rect = ReactDOM.domElementToObj(
getElementById(BoardBase.markerId(x, y)),
)["getBoundingClientRect"]()
(rect["left"], rect["top"])
})
),
}
}
, {columnPositions: emptyColumnPositions})
converts to
var match = React.useReducer((function (param, action) {
return {
columnPositions: $$Array.mapi((function (x, row) {
return $$Array.mapi((function (y, param) {
var rect = document.getElementById(BoardBase$SiReason.markerId(x, y)).getBoundingClientRect();
return [
rect.left,
rect.top
];
}), row);
}), emptyColumnPositions)
};
}), {
columnPositions: emptyColumnPositions
});
The same code in ReasonML:
React.useReducer(
(_, action) =>
switch (action) {
| UpdateColumnPositions => {
columnPositions:
emptyColumnPositions
|> Array.mapi((x, row) =>
row
|> Array.mapi((y, _) => {
let rect =
ReactDOM.domElementToObj(
getElementById(BoardBase.markerId(x, y)),
)##getBoundingClientRect();
(rect##left, rect##top);
})
),
}
},
{columnPositions: emptyColumnPositions},
);
converts to:
var match = React.useReducer((function (param, action) {
return {
columnPositions: $$Array.mapi((function (x, row) {
return $$Array.mapi((function (y, param) {
var rect = document.getElementById(BoardBase$SiReason.markerId(x, y)).getBoundingClientRect();
return [
rect.left,
rect.top
];
}), row);
}), emptyColumnPositions)
};
}), {
columnPositions: emptyColumnPositions
});
That is the code produced by the Rescript compiler. One works, one does not.
If you can't see the difference, let me help you out. Here is a screenshot of the diff.
The files are exactly the same.
I produced this code by runnning npx rescript convert -all
which is documented here:
$ npx rescript convert -all 🔥
— ReScript (@rescriptlang) June 30, 2021
This is an excellent script which up until this example, always worked when converting from Reason to Rescript. The conversion is one way though so if you like your Reason code, like I do, be aware of that.
Back to the point. The code that fails was the Rescript code. If you look at the two versions of Javascript, you will notice that they are exactly the same. What happened?
Per the Rescript docs:
In a dynamic language such as JS, currying would be dangerous, since accidentally forgetting to pass an argument doesn't error at compile time
So the Rescript docs are telling us that we might expect that using Rescript, your code might compile but then throw an error when its run.
So is the ReasonML code safer in that regard? I image that the above statement can be made about ReasonML as well but that for some reason, the ReasonML code happens to produce correct
Javascript while the Rescript compiler does not. Anyone with an idea on why this is happening, please do share. I would love some feedback on it.
In ReasonML, all functions are curried by default. I could tell you about it but @glennsl knows something about OCaml and ReasonML and explains it well here:
(. )
as used here, in function application, means the function should be called with an uncurried calling convention.
When used in a function type, like the type of resolve
here, (. 'a) => unit
, it means the function is uncurried.
Ok, so what the hell does that mean…
Or let Axel Rauschmayer, @rauschma tell you about on the seminal 2ality blog and twitter feed @2ality;
The fix for the Rescript code is easy is enough. Once you realize that the code might need to be curried by reading this post or through years of experience, well then you can curry the Rescript version and see what happens. You do that as follows:
let rect = ReactDOM.domElementToObj(
getElementById(BoardBase.markerId(x, y)),
)["getBoundingClientRect"](.)
(rect["left"], rect["top"])
Note the (.)
after getBoundingClientRect
. That is how you uncurry a function in both ReasonML and Rescript though the ReasonML version does not require it. It happens automatically. I guess this means that Rescript is mostly
curried by default. Again, if you need to uncurry
a function you need to add the dot as shown.
This kind of silent break down is exactly the reason why I took up ReasonML. I could not take chasing down another silent error in Javascript code.
The Rescript team is aware of this and have a section in the documentation addressing it. See Use Guaranteed Uncurrying in the docs.
Thanks for teaching @scottcheng, @rauschma and @glennsl.
Having just converted and updated the existing repo, I'm thinking that maybe we can avoid this altogether by structuring the function differently. I'll try to do that and update this post.
If you are into this sort of thing, here is a script I use to do a lot of the clean up.
19