Stream: ideas

Topic: optional chaining operator


view this post on Zulip Richard Feldman (Sep 21 2025 at 01:53):

it occurred to me that in the static dispatch world, an optional chaining operator would be nice

view this post on Zulip Richard Feldman (Sep 21 2025 at 01:53):

this operator is ?. in almost every language that has it, except Ruby - which couldn't do that because Ruby has ? as a valid identifier character, so foo?.bar already means (foo?).bar in Ruby because foo? is a valid name

view this post on Zulip Richard Feldman (Sep 21 2025 at 01:54):

Ruby considered .? instead but apparently Matz thought it would be too easy to mess up becasue it was so similar to ?. in every other language, so ended up going with &. instead

view this post on Zulip Richard Feldman (Sep 21 2025 at 01:56):

assuming we went with the syntax ~every other language does, then instead of writing this...

result = strings.first().try(|str| str.to_dec())

or this...

result = strings.first().try(Str.to_dec)

...you could just write:

result = strings.first()?.to_dec()

view this post on Zulip Richard Feldman (Sep 21 2025 at 01:59):

an argument against it would be that since strings.first()? already has meaning, strings.first()?.to_dec() should mean (strings.first()?).to_dec() - in other words, do an early return if strings.first() returns Err

view this post on Zulip Richard Feldman (Sep 21 2025 at 02:00):

but I think it's fine to require including the parens explicitly if you want that

view this post on Zulip Richard Feldman (Sep 21 2025 at 02:01):

and it's so universal that ?. is the operator that's used for this, it seems like the bar for choosing anything else would have to be super high (e.g. Ruby's "literally this can't work because ? is already valid in identifiers and super commonly used at the end of them")

view this post on Zulip Brendan Hansknecht (Sep 21 2025 at 02:37):

I definitely don't understand this or why it would be wanted over standard ? with normal . semantics.

view this post on Zulip Brendan Hansknecht (Sep 21 2025 at 02:38):

Is there an equivalent in rust or zig?

view this post on Zulip Brendan Hansknecht (Sep 21 2025 at 02:55):

Actually, I guess this plays mostly fine with ? just a bit more indirect before returning

...

Probably looks confusing when multiline

view this post on Zulip Karl (Sep 21 2025 at 03:16):

Brendan Hansknecht said:

I definitely don't understand this or why it would be wanted over standard ? with normal . semantics.

The ? operator in Rust (and IIRC proposed in Roc) returns on Error/None. Optional chaining lacks the return part and short circuits the expression to null/undefined/None/etc. The JS expression document.querySelector('.foo')?.textContent ?? 'bar' should be roughly equal to query_selector(document, ".foo").map(|x| x.textContent).unwrap_or_default("bar") in Rust but the optional chaining operator can be used many times in an expression and they'd all use the same fallback which would require .and() in Rust.

view this post on Zulip Richard Feldman (Sep 21 2025 at 03:48):

right, "multiple operations that can fail followed by a default for the whole thing (instead of early return)" is a classic example of where it's nice

view this post on Zulip Brendan Hansknecht (Sep 21 2025 at 03:51):

Ah, I see

view this post on Zulip Brendan Hansknecht (Sep 21 2025 at 03:51):

Feels odd to me, but I have never used it

view this post on Zulip Brendan Hansknecht (Sep 21 2025 at 03:53):

Also, feels like it would be confusing multiline

x.first()?
  .to_dec()?
  .other_fn(123)?

view this post on Zulip Richard Feldman (Sep 21 2025 at 04:00):

hmmm yeah

view this post on Zulip Richard Feldman (Sep 21 2025 at 04:00):

that's a good argument against doing it at all :thinking:

view this post on Zulip Richard Feldman (Sep 21 2025 at 04:00):

. needs to be able to go on its own line, and today the above is valid and reasonable code

view this post on Zulip Richard Feldman (Sep 21 2025 at 04:02):

although I guess that kinda implies that ?. should probably already be syntactically valid - and equivalent to the newlines versions instead of working the way it does in every other language that has that :grimacing:

view this post on Zulip Richard Feldman (Sep 21 2025 at 04:05):

well, I guess except Rust

view this post on Zulip Richard Feldman (Sep 21 2025 at 04:05):

where ?. does the early return and I guess it hasn't been something I've seen be a problem in practice

view this post on Zulip Richard Feldman (Sep 21 2025 at 04:06):

which means if this were to be a thing, maybe it would make more sense to be:

result = strings.first().?to_dec()

view this post on Zulip Kiryl Dziamura (Sep 21 2025 at 06:15):

Richard Feldman said:

which means if this were to be a thing, maybe it would make more sense to be:

result = strings.first().?to_dec()

And if question mark in the end of an expression, it's an early return as well?

result = strings.first()?.to_dec()?

To me, it's all too subtle. You have to be very attentive to the position of the question mark. How would it look like in dynamics? When you write code and question mark starts changing its meaning?

I would prefer early return and chaining to be different operators

view this post on Zulip Kiryl Dziamura (Sep 21 2025 at 06:46):

On the other hand, it may be as simple as "if question mark in the end of expression - it's early return, otherwise - optional chaining". The last question mark would then propagate the early return to other question marks in the expression.

view this post on Zulip Kiryl Dziamura (Sep 21 2025 at 06:52):

Brendan Hansknecht said:

Also, feels like it would be confusing multiline

x.first()?
  .to_dec()?
  .other_fn(123)?

So in this example, no matter which step was failed, since there's question mark in the end - we know the earliest failure would be returned. If you don't want to early return - you can remove the last question mark so the result of the expression is Result

view this post on Zulip Kiryl Dziamura (Sep 21 2025 at 07:03):

My take is that early return is hardly needed in the middle of the chain. If it does - it's better for readability to split the chain in two even if language supports both optional chaining and early return in the middle

view this post on Zulip Brendan Hansknecht (Sep 21 2025 at 07:45):

Kiryl Dziamura said:

On the other hand, it may be as simple as "if question mark in the end of expression - it's early return, otherwise - optional chaining". The last question mark would then propagate the early return to other question marks in the expression.

Yeah, that is a simple way to explain it

view this post on Zulip Matthieu Pizenberg (Sep 21 2025 at 08:33):

It sounds like it would be confusing to have ? for two different (but close in some way) things. But it’s just a hunch. Hard to check without using the language as is going to be.

On a side note. It feels like with the current zig rewrite, the pool of people that can actively evaluate the usefulness of proposal X or Y is shrinking until that release is usable by a broad set of people. I suppose it’s a tension between getting more eyes on it and getting less friction to change. But some proposals like this one to me feel like they would benefit from getting more eyes on it and more practice.

view this post on Zulip Fabian Schmalzried (Sep 21 2025 at 14:19):

I'm generally in favour of a chaining operator. If one doesn't want early return, nesting a lot of .try(...) could be annoying.

x = a.b()?.c()?.d()? should still be equivalent to x = ((a.b()?).c()?).d()? - it just early returns the first error.

It just might be strange if d() does not return an error itself, because now you would still have to do d()? to get early return.

For

x = a.b()?

  .c()

I would just let the formatter format this to

x = a.b()

  ?.c()

and do chaining.

So the mental model would be that ? at the end is early return, any ?. in the expression is chaining.

view this post on Zulip Kiryl Dziamura (Sep 23 2025 at 10:32):

There's only one confusing case from what I understand:

y = (x.a?).b # early return
y = x.a?.b # chaining

in such case with parens I think it makes sense to generate a warning that this might be confusing and suggest either drop parens, or move x.a? to a separate expression:

tmp = x.a?
y = tmp.b

the following expressions are equal and formatter may automatically drop parens there:

y = (x.a?).b?
y = x.a?.b?

the other option is to use (e.g.) &. for optional chaining and ?/ ?. for early return, so it's always clear what may cause function to fail:

y = x.a&.b? # this line may cause early return
y = x.a?.b? # this line as well
y = x.a&.b # never leads to early return

Some potential weird powers
&. may also be desugared to .flat_map if it's implemented on type

Result.flat_map : Result(a), (a -> Result(a)) -> Result(a)

a.flat_map(|x| x.b) # desugared from `a&.b`

so one can flat map lists (if they hate their reviewers :thinking: )

word_lenghts = text.split("\n")&.split(" ")&.len

a verbose alternative is a shortcut syntax for callbacks with single argument:

y = x.a.flat_map(.b)?
word_lenghts = text.split("\n").flat_map(.split(" ")).flat_map(.len)
strings
    .flat_map(.first())
    .flat_map(.to_dec())
    .flat_map(.other_fn(123))?

Last updated: Jun 16 2026 at 16:19 UTC