I agree. I've been wanting to write-up something for a while now, but I've been pretty busy. My feeling is that this thread has brought up four important questions:
- Do we want this feature at all? Will it impact learnability?
- Drilling into the motivation more seems important.
- My take is that this can be a win for ergonomics and learnability (which I see as distinct, but related, goals)
- As far as ergonomics goes, I often find the need to add
Ok
to be kind of a "mental impedance mismatch" -- I forget it, then get an obscure compiler error, then add it. I think this is because code likefoo()? + bar()?
never has to talk aboutErr
explicitly, so in a way it's odd to have to talk aboutOk
- As far as learnability, that same mismatch means that to learn error handling one has to fully grok its corners. I think it could be very useful to think of error handling as "exceptions that one must manually propagate at each level" and then, once you've learned about enums etc, understand how that works under the hood.
- But obviously things like Swift can act as a counter example: at some point, particularly when the abstraction is too "leaky", syntactic sugar just means you have to learn both the sugar and the underlying thing, and that's not a net win for learnability.
- Given that we want this feature, should the source code expose the "inner type" (e.g.,
T catches E
) or "outer type" (Result<T, E>
). This applies equally toasync fn
- I don't know the answer here. I think I'd like to try it both ways.
- I can see that using the "outer type" might have the same mental impedance problem ("I return a value of type
T
, but my return type is declared asResult
"). - On the other hand:
- I can see that makes the transition to full understanding more smooth;
- It helps when reading the signature of a fn you are about to call
- It eliminates a potential inconsistency with rustdoc
- It allows smooth support of
Option
- It has precedent from other TypeScript and C# for
async fn
at least
- I lean towards writing the "outer type"
- Given that we want the feature, how should we denote that you are using it? This is not independent from the previous question.
T caches E
(assumes internal type)try fn foo() -> Result<T, E>
(assumes external type)catch fn foo() -> Result<T, E>
(assumes external type)fn foo() -> Result<T, E> try { ... }
fn foo() -> Result<T, E> catch { ... }
- There are also blocks to consider:
catch R { ... }
try as R { ... }
?
- I think consistency with
async fn
is probably valuable here- note that
async { ... }
blocks are also a thing
- note that
- Given that we want this feature, is using the trappings of exception handling helpful or harmful?
- There is after a key difference in the semantics of this feature: errors do not automatically unwind through multiple frames but rather must be manually propagated with
?
- There are also implementation differences (e.g., we don't use typical unwinding techniques) but those are less imp't to my mind; after all, exceptions can be and sometimes are implemented using hidden return flags
- The core question is, will using "exception terminology" be helpful to people learning or confusing?
- I tend to think it is helpful: I think having something familiar makes it easier to learn the ways that error handling is different, in short.
- This isn't always true. The match between the syntax and the concepts have to be close enough. I think they are in this case.
- This does imo argue slightly against the keyword
catch
(as opposed to saytry
) -- using familiar keywords but in an unconventional way means that now you sort of have two things to learn. I'm not sure that's a net win.
- There is after a key difference in the semantics of this feature: errors do not automatically unwind through multiple frames but rather must be manually propagated with
Anyway, that's my best summary of how I see the state of the thread just now. I do hope to find the time to write out thoughts in more detail.