I was playing a bit with code generation and I noticed an interesting behavior. Take the following simple code (partially stolen from this thread):
module Option = {
let flatMapU = (x, f) => {
switch x {
| Some(x) => f(. x)
| None => None
}
}
}
let reciprocal = (. x) => x == 0. ? None : Some(1. /. x)
let indirect = Option.flatMapU(Some(2.), reciprocal)
switch indirect {
| Some(x) => Js.Console.log(x)
| None => ()
}
let direct = switch Some(2.) {
| Some(x) => Some(reciprocal(. x))
| None => None
}
switch direct {
| Some(x) => Js.Console.log(x)
| None => ()
}
let immediate = Some(2.)
switch immediate {
| Some(x) => Js.Console.log(reciprocal(. x))
| None => ()
}
Nothing particularly exiting, just applying reciprocal to a value if it’s Some. FYI: using the re-implementation of Option.flatMapU produces the same result as using Belt. Here the esbuild bundle:
(() => {
// node_modules/@rescript/std/lib/es6/caml_option.js
function some(x2) {
if (x2 === void 0) {
return {
BS_PRIVATE_NESTED_SOME_NONE: 0
};
} else if (x2 !== null && x2.BS_PRIVATE_NESTED_SOME_NONE !== void 0) {
return {
BS_PRIVATE_NESTED_SOME_NONE: x2.BS_PRIVATE_NESTED_SOME_NONE + 1 | 0
};
} else {
return x2;
}
}
function valFromOption(x2) {
if (!(x2 !== null && x2.BS_PRIVATE_NESTED_SOME_NONE !== void 0)) {
return x2;
}
var depth = x2.BS_PRIVATE_NESTED_SOME_NONE;
if (depth === 0) {
return;
} else {
return {
BS_PRIVATE_NESTED_SOME_NONE: depth - 1 | 0
};
}
}
// lib/es6/src/demo.js
function flatMapU(x2, f) {
if (x2 !== void 0) {
return f(valFromOption(x2));
}
}
var $$Option = {
flatMapU
};
function reciprocal(x2) {
if (x2 === 0) {
return;
} else {
return 1 / x2;
}
}
var indirect = flatMapU(2, reciprocal);
if (indirect !== void 0) {
console.log(indirect);
}
var x = 2;
var direct = x !== void 0 ? some(reciprocal(x)) : void 0;
if (direct !== void 0) {
console.log(valFromOption(direct));
}
var immediate = 2;
if (immediate !== void 0) {
console.log(reciprocal(immediate));
}
})();
As you can see, the bundler is able to see through valFromOption in case of immediate. However, in the other two cases it’s unable to optimize anything. The direct case is pretty bad, because instead of removing some, the branch and valFromOption, we have all the overhead.
Said that, this is probably more a topic for the optimizer than the rescript compiler, but I am wondering: is there something that could be done to help the optimizer? I am asking because a nice rescript codebase would use option (and result) all over the place, and even if the major performance issue are related to everything else (i.e. DOM manipulation), it would be nice if some low-hanging fruits could be available to have better codegen.
) about simple cases: if I see that for a micro-test that there are a set of operations that could be avoided, then it is a plausible opportunity for optimizations. If, for cases like this, nothing can be really done to improve, it is fine. But if the opportunity can be taken, then it’s right to do so, because at the very end very small performance gains sum up, and the resulting code could be significantly faster, even if by 1%.