There are so many valuable ideas in this thread, however, I would like to add a new idea that no one has mentioned.
Why not add an automatic “wrapping” ability for all enum types? I presume we use box keyword to do this. (box can generate different types as already designed, in different contexts)
It works as below:
enum E {
L(i32),
R(String),
}
let var : E = box 1; // wrapping i32 using E::L(i32)
let var : E = if cond { box 1 } else { box s };
fn foo() -> Result<P, Q>
{
return box p; // if typeof(p) == P, then it will converted to Ok(p)
}
fn foo() -> Result<N, N>
{
return box n; // if typeof(n) == N, compiler can't determine whether to use Ok(n) or Err(n). This is compile error
}
My main problem with the catch R syntax is that I think its harder to spot when its being used. I think T catch E is more visible, and this seems valuable to me.
I’m not particularly concerned about not seeing Result in the return type, any more than I am concerned about not seeing impl Generator in the return type of an async function. Users will learn this once & I don’t see it seriously hindering productivity before learning it.
My biggest complaint with it is that it means return Some(4) and return None do drastically-different things in a function that's -> Option<Option<i32>>.
(And there definitely shouldn't be a From::from in there, since that keeps things like return 4; from working normally.)
But is that function-level catch or a catch expression? It matters if there's a return in the closure.
It needs to be a bit magic, though, so code is more similar between fallible and infallible functions.
I think counting characters is absolutely the wrong measurement here. The big advantage I want out of this is that it becomes normal to always type the ? after something you know can fail as the first pass, allowing the first pass at the method to be written just like it was infallible.
@vorner IIUC, you’re trying to say that shorter edit-distance maybe a liability? So if I wanted to start using error handling for a function, it shouldn’t be possible to partially make those edits and still compile. Is that what you are getting at?
I guess that's part of it. Not the whole (the above already had error handling, just tried to rewrite it to the „new“ system), but yes, Rust is generally good at being clear about your intention (that Err(Base) being success isn't) and about „when it compiles, it's almost certainly correct“.
It may be worth revisiting the idea of try { .. }
Yes, please :-). I still don't feel persuaded there's a problem in explicitly marking „I terminated successfully“, but at least, try doesn't have that congitive dissonance between what is written and happens, like catch.
Furthermore, a little bit of syntax bike shedding from me: could we place the keyword directly in front of the brace? eg:
It is clearer this is about the block of code than about the interaction with caller (eg. the block of code feels like a parameter to the try keyword, or like the try is flow control for it, similar to how else is just before its block.
There's a case for this in C++ ‒ eg int main() try { ... } catch(...) { ... }
Feels much more consistent, both with the stand-alone try (catch) block and with other flow controls.
I'm not sure about the next part, but maybe it could even be that:
Any block whatsoever could be prefixed by try
If a type ascription of the result is needed, it could be done with the turbofish (just to feel the same and make it optional when not needed).
while let Some(value) = iter.next() try::<Result<(), Error>> {
}
Why not add an automatic “wrapping” ability for all enum types? I presume we use box keyword to do this.
Please, no. First, the Try trait seems more flexible and picking just on enums seems a bit like a hack. Second, the word Box has a very very different meaning in Rust and reusing it could be confusing.
The advantages i see in the original proposal is not that you use less
characters, but that treating the happy path as the normal return path has both
pedagogical and ergonomic advantages which I have tried to articulate.
I would be a lot more sympathetic for the proposal if it would really
feel like an abstraction and not like syntactic sugar for just 'Result'.
Just thinking about it, 'catch' might even be a bit like Rust's Haskell-do-notation.
I’m not particularly concerned about not seeing Result in the return type, any
more than I am concerned about not seeing impl Generator in the return type of
an async function.
I can't say that I like the bahaviour of async functions either.
That makes sense. I don't really like -> catch R { either -- but I do like the R bit of it still, though the next part gives me a bit of pause...
...hmm, so I think we should probably aim for consistency here, in any case. That is, either we want to show the "external" type or the "internal" type, but not sometimes one and sometimes the other. This seems to imply:
async fn foo() -> impl Future<T, E> { ... }
or plausibly impl Generator, but hopefully we could make either work.
I think the real question (that I am still wrestling with) is whether having the user write the full return type will help with learnability or not. I could see an argument either way.
The more I think about this, the less I like it, sorry.
First of all, I am used to think of a function as 2 parts: signature and behaviour. This proposal moves part of the behaviour into the signature, making it harder to reason about.
The proposed fn(params) -> Result<T,E> catch { ... } mitigates it a bit, as it is effectively the same as fn(params) -> Result<T,E> { catch { ... } }, but still offers very little in terms of clarity.
The other problem is that if we are trying to optimize the positive path by saying return n; automatically wraps it into Ok(n) and fail/throw/err "error" returns Err("error"), we are effectively introducing exceptions, so just call them exceptions and use the standard throws and throw, if not even try and catch.
I honestly cannot see any difference.
I am pretty sure that with time everyone will write functions with the catch block, effectively bubbling the errors all the way to main(), like exceptions.
I hugely prefer the explicit return Ok(n) and return Err("error") forms.
People already bubble it up to main, with ? and crates like failure. I think that's OK.
The big difference is the annotation with ?. The huge pain of C++ is exception safety, because an exception can fall just out of everywhere, so you need to be careful about where you can exit the function. If you have a block of code containing no ?, you can be pretty sure it won't exit there, so you can have temporarily inconsistent data there. Have you ever participated in a heated discussion of three senior C++ developers, arguing if this function provides correct exception guarantees and which these actually are?
Another difference is, you can do .map() or unwrap_or_else on results, but not on exceptions. I like the flexibility of using what is more appropriate at the time.
I think when considering making changes of this scale, not only should we consider the possible benefit to completely new users, we should also consider how it affects existing language users.
Does this change add more confusions in deciding what to use?
Does this add a “this language is still in flux” impression to potential serious users in the industry?
Does this change focus on removing superficial complexity like the number of characters typed while adding internal complexity like additional reasoning around special cases?
In the meantime, we should also keep in mind that we already have a large portion of resources out there in places like StackOverflow. Thus we should ask whether there is negative impact to new users as well:
Does this change increase the need to update existing resources to avoid confusion to new users?
Does this change add confusion to new users already starting to get used to simple idioms?
As we should break compatibility if we are fixing bugs and soundness issues, if we are adding real expressive power to the language, or making language significantly more complete or consistent, we should consider changes of such scale. Otherwise we should have a second thought.
Exception safety in C++ is both more difficult and easier. Yes, you must assume that most functions might throw, but functions in rust might panic. In C++ there at least is the noexcept operator that statically checks that the expression (function) cannot throw, and neither can its callees.
I think we should not use the terminology of exceptions in rust because in rust these things are not the same – return values and panics are separated. We might one day add a keyword or some other static guarantee for expressions that will not panic, but no keyword is needed for an expression that cannot fail – you know that by simply looking at its type.
If the type of an expression is Result<T, E> then you know it can fail. I don’t like the idea of a special syntax for describing the type of expressions that are also functions (i.e. -> T catch E). Expressions are expressions and types are types – functions are not so special, imo.
+1 on that. That's why I advocate for (if we even need it) placing the try (or catch or whatever) or whatever in front of the function block, so it acts the same as on any other block.
Furthermore, does it make sense to be able to put it in front of (non-block) expression? Like:
No, I explicitly meant without the braces. What I mean is, can try take general expression and modify handling in that, with a block being special case of expression?