maybe expect Ok { // start function body
or default Ok in { // start function body
I agree the word catches
fits less well. I am mildly worried that we are "inverting" the sense of catch
anyway -- I know it makes sense from a certain perspective, but I still think it will surprise people.
What about?
fn foo() -? io::Result<()> { /* stuff */ }
This might confuse people from other languages (like Java, C++), where something like
try {
...
} catch (FooException fe) {
...
}
only catches FooException
s. In contrast, this construct catches all early returns and just specifies their type.
I really like the proposal for
fn foo() catch R { ... }
I'm not a big fan of ok-wrapping (I like explicitly writing Ok(t)
), but I think having the catch
in the signature helps. I like having the whole type and not hard-coding in Result
. (Also, perhaps we can omit the ->
if there is catch
?)
@bbatha @Centril Mixing ->
and ?
seems to result in some pretty weird-looking syntax IMHO. I would actually like a keyword for this, which is more syntactically obvious to me.
Not a fan of omitting ->
, I don't even like that you can make fn main() -> ()
into fn main()
(but we can't remove that ability now..). There's certain beauty in function arrows for functional programmers since it looks like math =)
Iâm trying to think about the best way to explain why that feels
different; please pardon the smoke coming out of my ears as I think
really hard to introspect that feeling.
If I see something like this:
fn foo(s: String) -> &str {
// ...
{
// ...
return &s;
}
// ...
}
I donât see the change of &s
from &String
to &str
as part of the
return
, I see it as part of the &s
, and a fairly natural one;
effectivly, &str
is what you typically get when you apply &
to
String
. That doesnât feel like magic attached to return
, it feels
like magic attached to &
.
Iâm assuming something like
fn foo() -> u32, Error {
if bar() {
return 0;
} else if baz() {
throw MyError;
}
7
}
wouldnât work ?
Itâs kind of both both, isnât it? &String
only gets auto-derefed to &str
because itâs used in a context that expects &str
. That is, let s = &string
produces s: &String
while let s: &str = &string
produces s: &str
.
I did like the comparison with C# async, which retains the full return type in the signature but changes the meaning of return
in the body. I think whatever syntax we end up with should probably emphasize that itâs a body-level transformation and not part of the function signature.
Perhaps rather than thinking about Ok
-wrapping as a coercion it would be better to think of it as a wrapping behavior. I certainly donât want both return 0
and return Ok(0)
to work in the same context, but I do like the idea of catch
blocks and functions letting you write non-Result
-y code internally.
I feel like this has the same problem as T catch E
: there doesnât appear to be an obvious generic way for this to work with any return type R: Try
âŚ
maybe that isnât a deal-breaker kind of a problem ? In the sense that we rarely see in languages with exceptions, people asking for a way to change the vm-internal types handling the successful return and erroneous exceptions ?
That is fn foo() -> u32, Error
being sugar for fn foo() -> Result<u32, Error>
which would be the most common use case, but one might still use return
and throw
in a fn foo() -> Try<u32, Error>
if one needed to, just like the C# async example Niko showed ?
Hmm⌠-?>
doesnât look all that weird to me⌠I think terseness here aids in bribing people away from .unwrap()
âŚ
But hereâs perhaps a better idea:
Why not replicate the proposed throw
/ fail
with succeed
semantics intead?
So letâs introduce:
fail $expr <=> return <return_type>::from_error(From::from($expr))
and
pass $expr <=> return <return_type>::from_ok(From::from($expr))
// both fail and pass are 4 characters long, as a nice aesthetic bonus =P
By doing so, we donât have to change anything in the function signature, which is nice. It also enables very local reasoning about the semantics.
Reworking the original example:
fn foo() -> Result<i32, Error> {
if bar() {
pass 0;
} else if baz() {
fail MyError;
}
quux()?;
pass 7
}
Some potential keywords instead of pass
:
succeed
ok
deliver
-
triumph
(OK; this is a jokeâŚ)
(yeah, I totally checked a dictionary⌠)
Hmm⌠I actually quite like that proposal
I think ok
is nicer than pass
, but it might cause confusion with Result::Ok
⌠not sure what others think, thoughâŚ
Thanks =)
I first wrote my comment with ok
, but then realized that the fact that both fail
and pass
are 4 characters long makes for very nice symmetry and beauty wrt. alignment, so I went with pass
.
Since the Try
trait has not been stabilized yet, we can also change the names of the associated types into Fail
and Pass
and methods into from_fail
and from_pass
to make things more consistent.
To continue the bikeshed⌠I think pure
might be an even better name than pass
and corresponds well to:
- http://hackage.haskell.org/package/base-4.10.1.0/docs/Prelude.html#v:fail
- http://hackage.haskell.org/package/base-4.10.1.0/docs/Control-Applicative.html#v:pure
fn foo() -> Result<i32, Error> {
if bar() {
pure 0;
} else if baz() {
fail MyError;
}
quux()?;
pure 7
}
Using pure
also has the advantage of not confusing python devs.
Another advantage is that pure
is a reserved keyword already.
N.B: In Haskell pure :: Monad m => a -> m a
has no early return for success like this since the language is not imperative, so this would be a difference, but Iâm sure Haskellers (like myself) could live with this
EDIT: The folks over at #haskell @ freenode
did not like using pure
for this - so I am now sure they could very barely live with this. So scratch pure
as a contender here imo.
This thread is quite long. I havenât read beyond the first 10 replies. I suspect someone has made my points, but here goesâŚ
I really like the concept of throw expr
. Coming from someone whoâs point of view is mainly object oriented (and mostly Ruby), I find that syntax to be much more clear whatâs going on than Err(expr)?
(tbh I hadnât even considered that as a possibility until reading it here. Iâve always written return Err(expr.into())
.
I really dislike the idea of -> T catch E
. I do not think that syntax obviously maps to Result<T, E>
, and from the perspective of someone reading some Rust code who encounters that syntax for the first time, it is not going to be something that is easily googleable. Additionally, I do not like the idea of the return type of a function affecting the behavior of return 1
(or even just 1
in some cases). This feels really non-local to me. Having to potentially scroll in my editor to determine what an expression does puts a bad taste in my mouth. (Yes, this is partially a straw-man, since that is already the case today with type inference, especially if the last line of the function is x.into()
, but I consider that to be generally bad practice and âdifferentâ).
Can you show a more concrete example of how other return statements would need to change when a function becomes fallible? I could see wanting to review and revise the entire function if it starts doing something fallible (though I also think there are many cases in which this would be an excess of caution), but nothing about the return statements in particular seems like they should be highlighted.
This is quite an example because of course we use the return type of a function to determine what return 1
means - we determine the type of 1
from the return type of the function. As has already been mentioned, we also perform deref coercions (which can run arbitrary code) and other coercions like this.
So could you move from arching statements like "the return type of a function should not affect the behavior of return expressions" - which are false regarding Rust as it exists today - to more concrete statements about why wrapping them in Ok is problematic?
So could you move from arching statements like âthe return type of a function should not affect the behavior of return expressionsâ - which are false regarding Rust as it exists today - to more concrete statements about why wrapping them in Ok is problematic?
Well I didn't say that, and I explicitly said in my reply that there were many cases that we do this which don't bother me for reasons I'm not sure how to quantify... But I guess I can try to figure out why this bothers me but deref coercions don't...
Every example you've raised is an example of type inference, which happens universally in the language. 1
always infers to u16
, regardless of it's a return type or a type parameter. Same for deref. Same for into
. This would be the only case where a coercion like this is not tied to type inference.
I think inferring integer/float types is very different from the other arguments. It's quite easy for me to see 1
as "oh yeah that's an integer of some unspecified size". Not to mention that inferring 1
as 1u16
vs 1i32
does not fundamentally change its meaning. Every other one of your examples has something at the call site which implies conversion could be happening. Deref requires &
. Into
requires .into
.
Thanks for further introspecting on what bothers you about it! Iâll think about your response more thoroughly, but I want to quickly correct one factual point: deref coercions are not type inference. Youâve returned an expression &String
and the function expects &str
; weâre not actually inferring the type of youâre return expression, weâre inserting code based on the relationship between that type and the type of the return value.
Iâve always thought of it as a form of type inference, especially since it doesnât apply to trait dispatch (one of the only places type inference doesnât apply), but that makes sense as a difference. Unfortunately my arguments arenât super technical, theyâre that my squishy feelings like one thing but not another.