15 ms·
The ? operator looks lovely. It's like the C# null conditional operator, but for the Result pattern: try!(try!(try!(foo()).bar()).baz()) Becomes foo(
by Scaevolus 10y ago
The ? operator looks lovely. It's like the C# null conditional operator, but for the Result pattern:
try!(try!(try!(foo()).bar()).baz())
Becomes
foo()?.bar()?.baz()?
Versus the Go equivalent:
a, err := foo()
if err != nil {
return nil, err
}
b, err := a.bar()
if err != nil {
return nil, err
}
c, err := b.baz()
if err != nil {
return nil, err
}
return c
- JoshTriplett 10y agoExactly. It isn't just that it's shorter; it fixes a right-left-right-left alternating reading order, turning it into a left-to-right reading order.
- Scaevolus 10y agoI think it also lets you turn this: try!(baz(try!(bar(try!(foo()))) into baz(bar(foo()?)?)? Which isn't as bad as the alternating order for chained evaluation, but cleans up a lot of the noise.
- JoshTriplett 10y agoTrue, though in some cases you may want to write that one as: foo().and_then(bar).and_then(baz) That becomes a bit more verbose, but it reads left-to-right, and works better if you want to use a closure as one of the functions. Which form looks more readable in a given circumstance may depend on the nature of foo, bar, and baz.
- masklinn 10y agoIt has somewhat different semantics though, for instance with `?` all three functions can use different error types.
- richardwhiuk 10y agoThey have to be convertable though, because the function must have a single result code.
- lisivka 10y agoWhat if I want to do something like in following snipet: baz(foo)?("Cannot do baz, foo={}", foo).bar()?"Cannot do bar"; Is there a syntax sugar for such thing? If so, I will be converted to Rust.
- steveklabnik 10y agoIt would work with just the ?s if you had the right implementations of the error conversion functions. I have an example below.
- lisivka 10y agoCompiler cannot determine intent, so it will not be able to produce correct, user readable error message, in user language.
- steveklabnik 10y agoYes, that's why you end up writing an error type and the conversions between various errors.
- lisivka 10y agoImagine, you are taking exam. If you found incorrect question in exam, then it is a bug. If you answered all questions but wrongly, then it is a failure. If you answered correctly to all answers but examiner said that you failed, then it is an error. In case of bug, code must panic. In case of failure, code must return a result, which clearly says that this is the failure. Try next time. In case of error, code must print informative description of error, bunch of data, and maybe some hints to operator, so he will be able to fix it. We are talking about errors, right? Why I need conversions to display bunch of strings with descriptions and variables to an operator? Just let me collect strings and print them to a log.
- steveklabnik 10y ago
- bluetech 10y agoHas anyone suggested some sort of postfix macro syntax? I don't have one in mind, but perhaps there is some nice looking, viable one. That might have been a more general solution than a new operator.
- JoshTriplett 10y ago> Has anyone suggested some sort of postfix macro syntax? Yes, numerous people. A few different discussions have gone by; it has many potential applications, but it introduces several aspects of syntactic weirdness and parsing weirdness, so it'd require a lot of care to introduce if at all.
- zalmoxes 10y agoAs a Go developer, I'm curious how is the error handled when foo() or bar() fail? Could you show an example where `bar()` returns a re-triable error or a fatal error etc? something where I have to make a decision other than returning it up the call stack?.
- Dobbs 10y ago`try!(foo)` is roughly equivalent to this (invalid) code: foo().unwrap_or_else({ |e| return Err(e) }) Or in Go terms: a := try!(foo) is equivalent to: a, err := foo() if err != nil { return nil, err } Basically it will take the result of foo, and if foo returns an error it will return the error itself. https://doc.rust-lang.org/beta/std/result/ https://doc.rust-lang.org/beta/std/result/ has lots of examples of alternative ways you can deal with the error case, or the Ok() case of a result.
- zphds 10y agoIt's worth mentioning that try! is a macro that basically takes that expr and turns it to something similar to your go example.
- steveklabnik 10y agoWhen they fail, it's like "return err"; that is, they just return the error value. You can only make it return up the call stack, that's it. It's for recoverable errors, not fatal ones. And the calling function would check for the error case and re-try if that's the behavior it wants.
- tsuraan 10y agoSo do all the functions have to have the same error type as the function that is calling them? I don't know rust, but in haskell notation, if a function returns "Either x y" would it then only be able to use the "?" notation on functions that themselves return "Either x z", and not on functions that return "Either a b" (so, failure sides have to match, but success sides are flexible)? EDIT: Answering my own question: https://github.com/rust-lang/rfcs/blob/master/text/0243-trait-based-exception-handling.md https://github.com/rust-lang/rfcs/blob/master/text/0243-trai... makes it pretty clear; the error types must be the same. Which completely makes sense.
- bresc 10y agoi actually don't think the Go version is bad. I can very clearly see what is happening and I can also do something with the error
- vvanders 10y agoNothing keeping you from using the same approach(or even some of the really nice mapping functions), it just lets you get rid of the boilerplate that you see there when you want to.
- zalmoxes 10y agoThe difference is that if I thought the Go style is easier to read/debug I couldn't prevent you from writing it in the complex one-liner way. The go way is a little more verbose/uglier but we are both going to do it in the same way as will the 30 other developers working on the project.
- Etzos 10y agoI don't think it's fair to say that the option Rust gives you is more complex. It's a syntactic convenience which reduces the chance of a someone typing a mistake in the longer form. Regardless of how simple the language, there is always more than one way to do things. Rust is just giving you the choice to eliminate a lot of boilerplate if you so desire. In reality most Go users will opt to use the style mentioned above instead of something more verbose/complex and most Rust users will likely opt for the new ? operator.
- cpuguy83 10y agoThe only thing bad about Go's version (other than the usual repetitive code complaints) is it's easy to ignore (ie forget) to handle an error. And really much of the error handling in Go is by convention (ie, `if err != nil { return nil, err }`) rather than actual language semantics. Honestly it would be nice to be able to always return the error (without special macros or symbols) if there is one unless I've explicitly setup a handler block.
- namelezz 10y agoI find the Go version to be easier to debug though.
- junke 10y agoI hope so, because it is more subject to accidental copy-paste bugs.
- pcwalton 10y agoWhy? Can you explicitly name a type of mistake that might happen and explain how Go solves it in a way that Rust does not?
- namelezz 10y agoIt's not really a mistake. Just coding preference. For example: do_A()?.do_B()?.do_C()?. With the one line statement like that, when weird things show up, I don't really know if it's caused by bugs in do_A, do_B, or do_C. The linear layout of Go code lets me go through each do_X call line by line and setting break points is also easier. The problem here is when I need to investigate the chain do_A()?.do_B()?.do_C()?., I may have to rewrite that statement.
- pcwalton 10y agoI agree that we should have good debugging tools for this (in particular, break on error return). But note that you can break on function calls and use "finish" to effectively go call by call to see where the error is today.
- globalgobble 10y agoThank you for showing the "simple is beautiful" in favor of GO.
- moomin 10y agoThat's because they're both >>= in Haskell. So is SelectMany in C#. Or to put it another way, it's taking an operation that does this: a -> t of b And turns it into one that does this: t of a -> t of b There's a formal name for "t"s that support this operation. I'll let you guess what it is. :)
- tatterdemalion 10y agoThis is inaccurate. While Result is a monad, `?` in Rust is not the monadic bind. That's the `and_then` method.
- deleted 10y ago[deleted]
- lomnakkus 10y agoWhile it may be convenient, it's truly worrying such (EDIT: was "many") syntactic special cases are going into Rust at this point. (It may be the case that these things absolutely cannot be handled in a systematic way in a no-GC/no-runtime language, but I'd really like to see some evidence that this is the case.)
- steveklabnik 10y agoWhich ones specifically worry you? I can't think of many, but I am also incredibly biased.
- lomnakkus 10y agoSorry, phrasing was bad. I've edited.
- steveklabnik 10y agoNo worries, it happens. What worries you about it? What things need to be "handled in a systematic way"? Are you just generally opposed to any new syntax in languages or something?
- lomnakkus 10y agoSee my other comment; https://news.ycombinator.com/item?id=12924880 https://news.ycombinator.com/item?id=12924880
- Manishearth 10y ago> how many I can only think of one redundant feature, and that is the ? operator. This is because `try!` gets used very often in in a lot of code. I can only think of one syntactic feature (redundant or otherwise) that doesn't get used often and that is HRTB(`for<'a>`), which needs to be there to be able to specify some types correctly (but is very rare).
- 10y ago
- QuantumLogic 10y agoI don't get why they'll let the language be abstracted in that way, but are against having ternary operators. It's the one thing that's somewhat bugged me about Rust. Aside from this one minor thing, I"ve been really loving the language itself. let greater = a < b ? b : a; Is a lot better than having to write this all the time: let greater = if a < b { return a; } else { return b; };
- steveklabnik 10y agoThis doesn't duplicate anything in the language; it moves a stdlib macro to be included in the core language. Since if is an expression, ternay operators would be wholly redundant.
- QuantumLogic 10y agoThe whole point of it would be to have a higher level of abstraction which makes codebases more concise. Avoiding duplication for the sake of avoiding it is as nonsensical as avoiding 100 dollar bills since we already have 100 single dollar bills that could make it up.
- masklinn 10y ago> The whole point of it would be to have a higher level of abstraction What are you talking about? A ternary isn't a higher level of abstraction, it's at best the exact same thing with a different syntax, at worst it's a more limited version of the same.
- masklinn 10y agoBut…. these are two completely different statements, the equivalent Rust is if a < b { b } else { a } why are you adding sprurious returns? And if you need to get the minimum of two values so much that actually impacts you, write a `min!` macro?
- shepmaster 10y ago
- Animats 10y agoVersus the Python equivalent: try: foo().bar().baz() except Exception as message: return message
- prirun 10y agoI think the Python equivalent is just: foo().bar().baz() ie, let someone higher up the stack handle the error.
- paulddraper 10y agoIt's true. Rust doesn't have exceptions, for many reasons. One is non-locality of exceptions. Even gotos but bounded by a function. Exceptions, however...those can take you anywhere. But the other side, as you point out, is that exception many common patterns are much made more succient.
- Arnavion 10y ago>One is non-locality of exceptions. Even gotos but bounded by a function. Exceptions, however...those can take you anywhere. I've never understood this argument. Where does `try!()` take you if there's an error? To the caller. If the caller also has a `try!()`, where does that take you? To the caller's caller. Where does `throw new FooException()` take you? To the caller. If the caller doesn't have a `catch(FooException)`, where does that take you? To the caller's caller. Edit: Note to commenters: I'm not arguing about explicit vs implicit returns. I do agree that explicit returns are nicer to read and reduce the surface area of a language. My comment is solely for the "Exceptions can take you anywhere" sentiment.
- Manishearth 10y agoExplicit vs implicit basically. You don't get to ignore the error. If the caller does have a try, it propagates. If the caller doesn't have a catch, it propagates. This becomes important because now your code no longer has invisible escape hatches 20 levels deep. You design your function with specific points where it may stop executing. You control when your function returns. You can also store the error value and handle it later (Servo makes use of this pattern often -- e.g. a network request with a body of Err, which needs to be handled when we try to read the body). > Where does `try!()` take you if there's an error? To the caller. If the caller also has a `try!()`, where does that take you? To the caller's caller. You can say this about return, too. returns and try both take you to the caller. The caller then bubbles you up, because it explicitly returns or tries.
- iand 10y agoHow do you decorate the errors? In Go I might write a, err := foo() if err != nil { return nil, fmt.Errorf("failed to foo: %v", err) }
- beefsack 10y agoIf you want something like this, you'd usually use your own error type to wrap the errors returned from the functions you call. You can implicitly do the conversions and keep the code the same if you implement `From` or `Into` for the wrapped error type.
- ameliaquining 10y agoYou often won't need to change the error object from what foo() returns; Rust errors are generally structs or enums, rather than non-machine-readable strings. But if you do need to change it, it looks like this: foo().map_err(MyError::new)?
- cpuguy83 10y agogo errors are generally backed by structs as well (though not necessarily), even `fmt.Errorf("some error")` is a struct (called `errorString`). `Error() string` is just the interface that things that want to be errors must implement, which ideally gives you the human readable version of the error.
- saghm 10y agoIn Rust, I think it's more idiomatic to just implement the `Display` trait on you error type so that you can generate the string when you need it instead of at the time the error occurs
- dasfasf 10y agoI use error_chain let a = foo().chain_err(|| "failed to foo")?;
- masklinn 10y ago`?` will convert the original error to the one the caller returns so you can implement the decoration in the conversion if that's suitable, otherwise `Result#map_err` lets you convert an error "at the callsite" (and is a noop if there was no error). Alternatively, you can use error_chain: https://brson.github.io/error-chain/error_chain/index.html https://brson.github.io/error-chain/error_chain/index.html.
- hnakamur 10y agoIn Go, I use https://github.com/pkg/errors https://github.com/pkg/errors these days and wrap errors with stack trace. package main import ( "fmt" "io" "os" "github.com/pkg/errors" ) func main() { v, err := run() if err != nil { fmt.Printf("err=%+v\n", err) os.Exit(1) } fmt.Printf("v=%d\n", v) } func run() (*int, error) { a, err := foo() if err != nil { return nil, errors.WithStack(err) } b, err := a.bar() if err != nil { return nil, errors.WithStack(err) } c, err := b.baz() if err != nil { return nil, errors.WithStack(err) } return c, nil } func foo() (*a, error) { return new(a), nil } type a struct{} func (a *a) bar() (*b, error) { return new(b), nil } type b struct{} func (b *b) baz() (*int, error) { return nil, io.EOF // some arbitrary error for demo } This prints a useful stack trace like below and you can know where the error occurred. $ go run main.go err=EOF main.run /home/hnakamur/go/src/bitbucket.org/hnakamur/stacktrace-demo/main.go:31 main.main /home/hnakamur/go/src/bitbucket.org/hnakamur/stacktrace-demo/main.go:12 runtime.main /usr/local/go/src/runtime/proc.go:183 runtime.goexit /usr/local/go/src/runtime/asm_amd64.s:2086 exit status 1
- jstimpfle 10y agoThis is superficially nice, but let's not forget that we would hardly do this in practice. It's much more likely that we would write your Go version with some error handling or logging messages added in. Practical concerns aside, in Haskell you get these things from the Maybe monad without extra syntax or macros: foo >>= bar >>= baz
- webkike 10y agoI'm pretty sure that ">>=" is "extra syntax". The only difference is in Haskell any function with two arguments can be interpreted as infix.
- jstimpfle 10y agoIt's not built-in syntax, not even a macro. Just a binary function.
- saghm 10y agoI think this would actually be more comparable to the Either monad. Also, "without extra syntax or macros" is kind of a trivial distinction, as `>>=` is just a function, not some sort of magic. That being said, I really wish there were some sort of equivalent of `try` or `?` for options in Rust. I don't feel like I want monads in Rust in general, but when dealing with options, I definitely miss the Maybe monad.
- jstimpfle 10y ago"extra syntax or macros" refers to OP's example of "?". That ">>=" is not magic is exactly what I said (meant to say).
- saghm 10y agoI guess I'm not seeing why having a function rather than a macro is really that much of a distinction in this case. Either way, it's essentially just a namespaced block of code that compiles down to an AST fragment; I don't think the facts that one of them uses the stack and that they're compiled at slightly different times really make much of a difference here.
- sudeepj 10y agoI think the new operator works only with Result. Had it been for Option also, it would have been awesome.
- steveklabnik 10y agoIt can be extended for Option in the future, we're just not sure that we're doing it yet. (I personally am opposed, but I suspect I'm in the minority.)
- imtringued 10y agoWrite a function to turn Option into a Result.