Rescript-compiler: Allow js `null` as `None` option runtime value to improve js interop

Created on 8 Dec 2019  路  8Comments  路  Source: rescript-lang/rescript-compiler

Proposal

Currently options can be represented as undefined | <js_value> at runtime. To improve interop with JS I wonder if it would be possible to represent them as undefined | null | <js_value>.

This would allow binding directly to JS objects without having to use Js.Nullable types. Currently it seems dangerous to do so since it is common to use null and undefined interchangeably in JS code.

For example:

type fooResult = {
  a: option(string),
  b: string,
};

[@bs.module "some-module"]
external foo: unit => fooResult
// some-module.js

export function foo() {
  return { a: null, b: 'b' };
}

This would currently break at runtime and requires writing explicit code using Js.Nullable to convert to option. If option would support null as a runtime value for None then this would be possible, which I think is valuable to reduce code needed to support interop with JS.

Implementation

I'm not familiar with how bucklescript works internally but from looking at the generated js code for options I think what would be needed is to generate a loose equality check instead of a strict one when comparing an option with none. This would mean using someOption == undefined instead of someOption === undefined.

Some potential issues I can see is when using an option(Js.Null.t) but this is probably already handled for Js.Undefined.t.

It will also probably has a small perf impact since using == is less performant than ===.

Most helpful comment

@janicduplessis that was discussed during the design phase for option, and decided against. It would be difficult to go back now.

Another possible direction would be to make nullable as similar as possible to option, with near identical APIs but keeping types distinct.

All 8 comments

@janicduplessis that was discussed during the design phase for option, and decided against. It would be difficult to go back now.

Another possible direction would be to make nullable as similar as possible to option, with near identical APIs but keeping types distinct.

The thing we made the decision is that we need to make sure the code below is of same semantics

match x with | None -> t | Some _ -> f
if x = None then t else f 

We can introduce a new type to make nullable life easier (two options)

type 'a t0 = private | None | Some of 'a
type 'a t1 = Null | Undefined | Some of 'a

t0 does not distinguish between null and undefined while t1 will

Couldn't there be something like [@bs.nullable] that can treat nulls as Nones? I just spent way too much time on figuring this out, and ended up cluttering my user land code when simple binding should've been enough.

As an user of the binding, I really shouldn't need to care if something is null or undefined as both are so mixed up in JS and usually treated the same. Some times there is special meaning and some times there is not, so it would really make sense to have an option to decide what's the desired behaviour.

E: Actually this is what I was looking for: https://bucklescript.github.io/docs/en/return-value-wrapping

I see, thanks for clarifying this decision.

@bobzhang I assume the problematic part is the x = None equality check, which will compile to x === undefined? Like I said I have no knowledge of how this is implemented but would it be possible to add a special case for equality checks between options to solve this?

A new option type that handles both null and undefined would also work but getting regular options to work with null would be better in my opinion (slightly biased considering I鈥檓 working on a project that has a large js part :))

@villesau FYI genType does the conversion for you: https://github.com/cristianoc/genType#optionals
In case this is useful to unblock you. It can be used even if you don't have types in JS (with the untyped backend).
Just notice the conversion is not zero-cost.

@janicduplessis It is a bit more complicated, None could escape so that some ad-hoc processing does not apply.

if x = none_value then .. else ..

When we introduce a private type, this can be avoided

I'm not sure I understand the request correctly, but essentially the request is to let both null and undefined resolve to None, instead of having null be Some?

I'd like to note that this can lead to problems consuming JavaScript libraries where null and undefined are used for distinct values.

For example:

function request(options) {
 // When no retries limit is given default to 10. 
 if (options.retries === undefined) {
    options.retries = 10;
 }

 let status;
 let retried = 0;
 // Perform the request and retry the request
 // - until it's successful if options.retries = null (disable limit)
 // - until the retry limit is reached
 // Set options.retries = 0 to disable retries altogether.
 do {
   status = doRequest();
 } while (status !== "success" && (options.retries === null || retried++ < options.retries));

It is hard to preserve the semantics when you map None to two values: null/undefined.
When interop we have to deal with the complexity that JS has two null values

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bobzhang picture bobzhang  路  3Comments

jordwalke picture jordwalke  路  4Comments

cknitt picture cknitt  路  3Comments

paparga picture paparga  路  5Comments

tanaka-de-silva picture tanaka-de-silva  路  5Comments