[RFC] Automatic optional chaining

Or the nullish coalescing operator familiar from JS: ??.

3 Likes

Thank you all for your feedback!

I’ve updated the original post to include a link to a prebuilt npm package you can download and install locally in your project to test.

Many different discussions to have here, both philosophical and practical. If we start by focusing on the practical aspects, we’re curious to understand what your experience is when trying it on real projects.

So, please test it in your code base in its current form, and report your feedback, preferably via concrete examples. Strongly recommend testing it with the editor integration as well.

5 Likes

This syntax looks magical but reasonable to me. Great work!

1 Like

Just aliasing getWithDefault to or makes things a bit more legible?

How does the compiled JS output look like in different scenarios?

1 Like

Please do test it in your codebase, as @zth suggests.

There are some practical aspects that only become clear in that way. For example, if one wants to be completely explicit, then 2 operators, not 1, are required. Conceptually corresponding to map and flatmap.

On that topic, and this will be explored separately, there’s the opportunity to make option types closer to the way they are natively in JS.

  • Basically, one can explore making option<option<t>> the same as option<t>
  • The price to pay is that there won’t be a way to distinguish between None and Some(None). So that’s a semantic change. Natively, JS does not have that distinction.
  • The advantage is that the distinction between map and flatmap should just just disappear.
  • The other change is that the whole mechanism for runtime representation of nested options, with its own compiler optimisations which sometimes fire and sometimes do not, will disappear. Basically, it’s always optimised by definition as there won’t be boxed optionals.
7 Likes

Do we need an operator for flatmap? It doesn’t look relevant to the problem of accessing fields in optional nested records.

Please do test it in your codebase

I’ll try to find some time during the week :ok_hand:

2 Likes

See the type of b and the implementation of getB in the 2 cases below.
Those are the 2 get operators.

module Example1 = {
  type c = {c?: int}
  type b = {b?: c}
  type a = {a?: b}
  let getA = x =>
    switch x {
    | None => None
    | Some(y) => y.a
    }
  let getB = x =>
    switch x {
    | None => None
    | Some(y) => y.b
    }
  let getC = x =>
    switch x {
    | None => None
    | Some(y) => y.c
    }
  let foo = x => x->getA->getB->getC
}

module Example2 = {
  type c = {c?: int}
  type b = {b: c}
  type a = {a?: b}
  let getA = x =>
    switch x {
    | None => None
    | Some(y) => y.a
    }
  let getB = x =>
    switch x {
    | None => None
    | Some(y) => Some(y.b)
    }
  let getC = x =>
    switch x {
    | None => None
    | Some(y) => y.c
    }
  let foo = x => x->getA->getB->getC
}
1 Like

So much boilerplate over writing accessors…why?

module Example1 = {
  type z = {c?: int}
  type y = {b?: c}
  type x = {a?: b}
  
  // derivable, or easily provided by language?
  let getA = x: x => x.a
  let getB = y: y => y.b
  let getC = z: z => z.c

  let getYC = y => y->getB->Option.flatMap(getC)
  let getXC = x => x->getA->Option.flatMap(getYC)
  let foo = getXC
}

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