[RFC] Automatic optional chaining

Totally agree moving forward, on the details i would prefer ?. for two reasons:

  • Explicitly means that you are getting an option<>
  • javascript has the same optional chaining operator, and people which rescript targets may already know it.

The operator ?. exists both in Javascript and also Typescript, so would be natural for rescript to embrace.

4 Likes

A question for those requesting an explcit syntax to access values of nested option types:
How would you generalize your suggestion?

Rescript does not have any type specific syntax yet. (AFAIK)
Why do we need one? - While keeping in mind: How should any other nested type be handled?
Like result, promise or any (custom) type.

1 Like

How would you generalize your suggestion?

Rescript does not have any type specific syntax yet. (AFAIK)

Would be nice to be able to implement ?. and other extension methods as user space extensions, it’s very common for mainstream languages like C# or kotlin, is also coming to javasccript.

1 Like

I really like this option after thinking about it. How would this be implemented? I think if we can at a compiler level do not allow nested options (and enforce this), it would be great. If we need any way to describe nested data structures, we can always use regular variants, which is often a lot clearer anyway in these situations. The same restriction of not allowing nesting would be amazing to have for promises, because they also don’t have a runtime representation in JS. This will make the current (naive) promise implementation correct.

This option combined with an explicit syntax for optional field access would be a very nice addition to the language imo. The runtime advantage is also not to be underestimate, especially when working with react, because sometimes the boxing will cause values to not have referential equality, so kicking off effects when it’s not expected.

3 Likes

In fact there’s already a warning in the compiler for nested promises. And the same can be done for options.

2 Likes

This seems pretty cool! I did have some questions though.

How would this work…

  • Would the flattening only occur in cases where you use the optional chaining?
  • Or would all nested options be flattened?

If you’re talking about flattening options in general, wouldn’t this give the same problems as with the promises? (I know the promise discussion is a decided topic regarding the benefits of the zero-cost bindings outweighing the loss of type-safety, but for option, maybe it isn’t decided yet.)

Specific examples

Here are a couple examples that I would be curious to know how they would be resolved if nested options are flattened…

Js.Option.some

An example of a function that cannot be typed if nested options are flattened: Js.Option.some.

It’s current type is 'a => option<'a>. Here is some type “math”:

'a => option<'a>

// 'a can be anything so let 'a = option<'a>, then
option<'a> => option<option<'a>>

// By definition, option<option<'a> = option<'a>, so
option<'a> => option<'a>

So the Js.Option.some function would need to have these two types: 'a => option<'a> and option<'a> => option<'a>, which is impossible.

Belt.Option.flatMap

Here is Belt.Option.flatMap (type sig: (option<'a>, 'a => option<'b>) => option<'b>).

If we bind an option<option<'a>>, then in the binding function we expect to work with the argument of type option<'a>. But with flattening, we could end up with the ground value 'a rather than option<'a>. This would break type safety.

(Well, unless there are some things going on in the compiler where the nested options are always guaranteed to have a flatted representation anyway, then I suppose you wouldn’t run into runtime errors at least…but still, how to deal with it in the code?)

3 Likes

In fact, the option flattening idea does not need to be combined with auto optional chaining, I think.

The idea would be simply: whenever some field needs to be accessed, be it x.f or via pattern matching, if x has option type then this is considered as syntactic sugar for reaching inside the option and re-wrapping the result as required.

I guess this can be done in a way that does not require assuming that options are flattened, as “looking inside the option” and “re-wrapping” has a specified meaning also with the current treatment of options that are wrapped in a special way when nested.

@zth what do you think?

1 Like

One thing I thought of is that in JS optional chaining can improve performance and reduce the bundle size: Performance of JavaScript optional chaining · allegro.tech

Here’s a bit of rescript with pattern matching for an object with optional keys and values:

type foo = { bar?: option<string> }
type bar = { foo?: option<foo> }

let fn = (bar) => switch bar {
 | Some({ foo: Some({ bar: Some(s) })}) => s
 | _ => "nothing"
}

And it compiles to

// Generated by ReScript, PLEASE EDIT WITH CARE

import * as Caml_option from "./stdlib/caml_option.js";

function fn(bar) {
  if (bar === undefined) {
    return "nothing";
  }
  var match = bar.foo;
  if (match === undefined) {
    return "nothing";
  }
  var match$1 = Caml_option.valFromOption(match);
  if (match$1 === undefined) {
    return "nothing";
  }
  var match$2 = match$1.bar;
  if (match$2 === undefined) {
    return "nothing";
  }
  var s = Caml_option.valFromOption(match$2);
  if (s !== undefined) {
    return s;
  } else {
    return "nothing";
  }
}

export {
  fn ,
}
/* No side effect */

Playground:
https://rescript-lang.org/try?version=v10.1.2&code=C4TwDgpgBAZg9nKBeKBvKAjAhgJwPwBcUcYwAlnAHYA8AzsDmZQOYB8UAvgFCiSa7I0sBIWKkKNeHHbcuAGwjBYlQQApsOAJTJ2tAO5lgAYwAW-HGi5QAPlADKcALYRV6KUQfPX5j05e1tDk0gnShaKxsoADkqaCR2ACJKOGATJmYErg4gA

It would be nice if using ?. in ReScript compiled to ?. in the JS.

function fn(bar) {
  return bar?.foo?.bar ?? "nothing"
}

But it’s not supported by older browsers and nodejs versions.

Browser support is at 93%, and Node supports it since v14, which was released 3 years ago. I don’t see how that outweighs the benefit of a smaller bundle.

1 Like

If you need to target older browsers and node versions you could use a bundler. For the frontend I’m sure most people are already doing that.

This argument kind of goes the other way as well - if you need a more compact bundle size you should use a minifier first and foremost.

1 Like

I do agree with your argument, but I wonder what should be the limit for using “modern” JS features in the output. Maybe 93%/node version from 3 years ago is indeed too recent, though :thinking:

Yeah this is definitively something we need to figure out a good strategy for, when it’s worth using newer features and what the bar for introducing them is. In my view, it’s a matter of doing thorough research in at least these areas:

  • How widespread is support for the syntax and what’s our tolerance?
  • Is it faster or at least as fast as the current thing it compiles to? If it’s slower, is it worth adding anyway? Why?
  • What actual benefits does it bring and does the above justify the benefits?

Modern syntax can be great for sure (we compile async/await to actual async/await as an example) but we need to be quite careful and mindful about what we introduce, and why.

5 Likes

That sounds like a great approach to it!
Thanks for the great work you and the others have been doing in the language lately, btw. Every new feature makes me a bit jealous of people who work with ReScript :smile:

4 Likes

This code uses double options everywhere btw.
It should be { bar?: string }

The runtime representation of option chaining is something one can analyse separately.
For now, the focus should be on the ergonomics of the syntax extension, and whether there’s enough interest to support it.

3 Likes

I always vaguely assumed that there’d be a bsconfig option for target ES version, to control the features used in the JS output. Then people could choose to involve a bundling-for-legacy step in their build setup or not. Is that not something that’s planned to be configurable at all?

Something I would LOVE to see, even more than optional chaining, automatic or otherwise, would be optional chaining with pipes…

let example = apple(_)?->orange?->grame("fruit")->basket
3 Likes

This basically allows to skip all the nested switch|None cases and focus on the switch|Some cases, in other words, converts the binary tree of switch|option cases into a simpler linear sequence

2 Likes