Or the nullish coalescing operator familiar from JS: ??
.
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.
This syntax looks magical but reasonable to me. Great work!
Just aliasing getWithDefault to or
makes things a bit more legible?
How does the compiled JS output look like in different scenarios?
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 asoption<t>
- The price to pay is that there won’t be a way to distinguish between
None
andSome(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.
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
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
}
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.
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.
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.
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).
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.
Can this feature be enabled under an experimental feature flag?
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.
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.
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.