[RFC] Automatic optional chaining

I’ve gone on about this before but seems to be consistent with function arguments these optional fields should be c?: option<int> (or arguments should be like above, which i prefer)

Just to illustrate 2 access patterns.
And point out that an explicit ?. operator would let you write x => x?.a?.b?.c for one case but not the other.

1 Like

This does sound nice when you share these points.

I think OCaml and F#'s scoped monamic let binding provide a better solution for this in more flexible way.

5 Likes

Although the custom monadic ops are indeed powerful, they cannot substitute what Cristian is talking about (as far as I understand).

Having the monadic ops you still have to think about proper ordering of map/bind/return. Whereas the offered mechanism effectively vanishes difference between map and bind by automatic flattening of types like option (Some(Some(Some(42))) becomes Some(42) on its own) or promise (like it always happens in JS anyway).

So, monadic ops and this proposal are not interchangeable.

4 Likes

I think this is an interesting optimization, but it would have real consequences in the language for things that are not small edge cases. Like distinguishing if a key did not appear in a hash map vs the value being None (when the value of the hashmap is an optional).

1 Like

One of the most confusing error for new comers to the language (working at a company where people are onboarded from scratch to rescript), is when people try to access attributes of an optional record. This will do the intended behavior in this scenario. I AM a bit worried about the implicit behavior. You can change a type and implicitly the behavior of the code would change. I am a bit worried that this can introduce subtle bugs without the compiler complaining, so I would be tending a bit in the direction of explicit syntax. On the other hand I could be wrong and this would give a way better developer experience.

4 Likes

Can this feature be enabled under an experimental feature flag?

1 Like

This (automatically accessing nested record props) seems like a really nice improvement.
I currently don’t see the need for an explicit syntax.

Regarding the discussion about general flattening of types: This idea sounds like an interesting one. (especially in the context of js)
While I see many benefits, this could also change semantics unknowingly. - Sometimes it is important which nested level actually returned a value. Other times it doesnt really matter.

I’ll test the published package and see if it helps to make up my mind. :wink:

Anyway, thanks a lot for your hard work!

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?)

2 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.