it occurred to me that in the static dispatch world, an optional chaining operator would be nice
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
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
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()
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
but I think it's fine to require including the parens explicitly if you want that
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")
I definitely don't understand this or why it would be wanted over standard ? with normal . semantics.
Is there an equivalent in rust or zig?
Actually, I guess this plays mostly fine with ? just a bit more indirect before returning
...
Probably looks confusing when multiline
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.
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
Ah, I see
Feels odd to me, but I have never used it
Also, feels like it would be confusing multiline
x.first()?
.to_dec()?
.other_fn(123)?
hmmm yeah
that's a good argument against doing it at all :thinking:
. needs to be able to go on its own line, and today the above is valid and reasonable code
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:
well, I guess except Rust
where ?. does the early return and I guess it hasn't been something I've seen be a problem in practice
which means if this were to be a thing, maybe it would make more sense to be:
result = strings.first().?to_dec()
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
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.
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
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
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
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.
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.
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