How can I get rid of the runtime libraries?

The following 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: 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?

3 Likes

Those runtime functions are necessary to handle cases like ‘option of option’. Without them the code could be unsound i.e. throw runtime type errors.

Where in my code have I used ‘option of option’ and how to fix that?

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.

1 Like

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.

This is correct.

2 Likes

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.

2 Likes

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.

4 Likes

Thank you @johnj , that answers it.

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? :slight_smile:

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?

1 Like

Currently Rescript looks like this:

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.

Hi, I checked that it seems the type specialisation does not work for externals, can you file an issue on rescript-compiler? thanks.

Edit: it is indeed a missing optimisation, filed here: https://github.com/rescript-lang/rescript-compiler/issues/4930 – will have a look when I have time

7 Likes

Good catch! Thanks for filing the issue.

Hi, hopefully this should be optimized by this diff https://github.com/rescript-lang/rescript-compiler/pull/4960

6 Likes