type clickHandler = ReactEvent.Mouse.t => unit
let makeDefaultClickHandler = (href, evt) => {
open Core.History
ReactEvent.Mouse.preventDefault(evt)
browserHistory->push(href)
}
@react.component
let make = (
~href: string,
~className: option<string>=?,
~onClick: option<clickHandler>=?,
~children: React.element,
) => {
let onClick = Belt.Option.getWithDefault(onClick, makeDefaultClickHandler(href))
<a href onClick ?className> children </a>
}
…compiles to:
// Generated by ReScript, PLEASE EDIT WITH CARE
import * as Core from '@core';
import * as React from 'react';
import * as Belt_Option from 'bs-platform/lib/es6/belt_Option.js';
import * as Caml_option from 'bs-platform/lib/es6/caml_option.js';
function Link(Props) {
var href = Props.href;
var className = Props.className;
var onClick = Props.onClick;
var children = Props.children;
var onClick$1 = Belt_Option.getWithDefault(onClick, function (param) {
param.preventDefault();
Core.browserHistory.push(href);
});
var tmp = {
href: href,
onClick: onClick$1,
};
if (className !== undefined) {
tmp.className = Caml_option.valFromOption(className);
}
return React.createElement('a', tmp, children);
}
var make = Link;
export { make };
/* @core Not a pure module */
The runtimes seem unnecessary because:
if (className !== undefined) {
tmp.className = Caml_option.valFromOption(className);
}
…is equivalent to:
if (className !== undefined) {
tmp.className = className;
}
and:
var onClick$1 = Belt_Option.getWithDefault(onClick, function (param) {
param.preventDefault();
Core.browserHistory.push(href);
});
…is equivalent to:
var onClick$1 = onClick ? onClick : function (param) {
param.preventDefault();
Core.browserHistory.push(href);
});
What am I doing wrong here and how to can I get rid of the runtime libraries?
This is because you’re using the Belt.Option.getWithDefault function in your ReScript code, and the compiler can’t inline functions used across modules. You can eliminate this by either using a switch statement or by defining your own getWithDefault function to use instead of the Belt version.
let onClick = switch onClick {
| None => makeDefaultClickHandler(href)
| Some(x) => x
}
The valFromOption is a bit trickier because it’s related to how the compiler treats option types as special and which necessitates a small runtime cost. AFAIK, the best way to avoid that is by not using option altogether, e.g.: ~className: string="" would still let className be “optional” but wouldn’t use the option type.
It seems a bit weird that it’s encouraged programmers replicate the functionality of what’s considered the standard library. Are there any libraries like this already created by the community?
~className: string="" expect a string and sets it to empty string when omitted right?
To be clear, using switch has always been the recommended way of working with variants since it’s built into the language itself. The functions in modules like Belt.Option or Belt.Result are primarily for convenience. Using switch will always produce more efficient code.
There are alternative stdlibs, like Relude, but they will have the exact same drawbacks you’ve already described for the built-in stdlib; their functions will still need to be imported and they will introduce runtime.
Thanks, that’s good information there. I suppose it would only be possible to use a library without pulling in a runtime, when you copy it into your codebase and not use it as a dependency?
I’m now using ~onClick: clickHandler=makeDefaultClickHandler(href) and that looks much better already. Not sure what to do with the ~className. I mean <a className=""> isn’t exactly equivalent to <a ?className>.
It’s just the first component that I converted to Rescript. Do you think it’s avoidable pulling in the runtime in all cases or is it something that cannot realistically be avoided and I shouldn’t worry about it? (the runtime doesn’t seem to be big though)
You don’t specifically need to use an ‘option of option’ in your code, valFromOption is a general-purpose function that checks for the possibility of that happening and corrects it. It’s used to preserve type guarantees while also unboxing option types to reduce allocations, a good engineering compromise.
In that case, the runtime will be imported from inside your codebase rather than from node_modules. It’s no different from taking any npm library and copying it into your own codebase.
Note that alternative stdlibs are mainly replacements for Belt (and a few of the other modules that ReScript inherits from the OCaml stdlib). Modules like Caml_option are automatically included by the compiler when they’re needed, and they can’t be replaced.
In general, I would say it’s not worth trying to avoid. There are many scenarios where the compiler will automatically import helper functions (as we saw with Caml_option.valFromOption). Although it’s possible to avoid those on a case-by-case basis, you shouldn’t need to avoid them. As you mentioned, the runtime it adds is very small.
Using built-in option for optional props definitely seems more idiomatic so I will just keep using it.
@yawaramin I wish there was a compiler flag or file level directive that says -im-not-using-crazy-stuff-like-option-nested-inside-another-option-so-please-just-chill-with-the-runtime-libraries-ok?
Note we do have type based optimization, so if the compiler knows, it is a option<string>, it is going to be specialized.
What’s the JSX desugared code?
type clickHandler = ReactEvent.Mouse.t => unit
let makeDefaultClickHandler = (href, evt) => {
open Core.History
ReactEvent.Mouse.preventDefault(evt)
browserHistory->push(href)
}
@react.component
let make = (
~href: string,
~className: option<string>=?,
~onClick: clickHandler=makeDefaultClickHandler(href),
~children: React.element,
) => {
<a href onClick ?className> children </a>
}
Which gets compiled into:
// Generated by ReScript, PLEASE EDIT WITH CARE
import * as Core from '@core';
import * as React from 'react';
import * as Caml_option from 'bs-platform/lib/es6/caml_option.js';
function Link(Props) {
var href = Props.href;
var className = Props.className;
var onClickOpt = Props.onClick;
var children = Props.children;
var onClick =
onClickOpt !== undefined
? onClickOpt
: function (param) {
param.preventDefault();
Core.browserHistory.push(href);
};
var tmp = {
href: href,
onClick: onClick,
};
if (className !== undefined) {
tmp.className = Caml_option.valFromOption(className);
}
return React.createElement('a', tmp, children);
}
var make = Link;
export { make };
/* @core Not a pure module */
but I would like it to compile into:
// Generated by ReScript, PLEASE EDIT WITH CARE
import * as Core from '@core';
import * as React from 'react';
function Link(Props) {
var href = Props.href;
var className = Props.className;
var onClickOpt = Props.onClick;
var children = Props.children;
var onClick =
onClickOpt !== undefined
? onClickOpt
: function (param) {
param.preventDefault();
Core.browserHistory.push(href);
};
var tmp = {
href: href,
onClick: onClick,
};
if (className !== undefined) {
tmp.className = className;
}
return React.createElement('a', tmp, children);
}
var make = Link;
export { make };
/* @core Not a pure module */
I’m using ReasonReact. I don’t really know what the JSX desugared Rescript code produced by ReasonReact looks like but I guess it expects ?className property to be a general option<'a> type which demands the inclusion of caml runtime.