Live data from Hacker News

On Error Handling in Rust

lucumr.pocoo.org

31–40 of 82 posts

Re: On Error Handling in Rust

#31
In some ways it's heartening to see Rust working everything out for itself - but it's also painful to watch the language stumble on the same problems that we've already solved.

You're going to hit the same problem again with async, with transactions, and with resource management; indeed some of the stuff I've already seen about borrowing and the like seems achingly close to the same pattern. Introducing new sigils like ? for each use case is not going to be sustainable.

The nice way to solve this is higher kinded types, monads, and some kind of concise notation for composing them (e.g. Haskell's do or Scala's for/yield). Then you can do something like (Scala):

    def read_value(host, port) = for{
        sock ← TcpStream::connect(host, port)
        parser = Parser::new(&mut sock as &mut Reader)
        r ← parser.parse_value()
      } yield r
and this is generic and extensible; you avoid blowing your syntax budget because the ← syntax is reusable for any "context" that behaves in the same way (which turns out to be most of them), and also for user-defined types. There's no need for macros or magic method names (If you want the FromError functionality you can use a typeclass). Everything's implemented with ordinary types and objects behaving in the ordinary way.

Of course none of this is perfect - in Scala the ← syntax is "magic", implemented in the parser, and so the method names it invokes (map/flatMap) are also magic, and different languages have to fight over the best implementation for them. In Haskell the compiler knows about the Monad typeclass specifically, and the do notation is linked to that; if you were to write your own implementation of Monad, you wouldn't be able to use the syntax. There's plenty of room for innovation here, and I hope Rust eventually comes up with something better than either of those approaches. But an ad-hoc ? operator that calls a macro that calls a specially named method really isn't the way forward. I'm sure Rust can come up with something more generic and principled than this.

Re: On Error Handling in Rust

#32
I am not that familiar with Rust, however every time I see it I am more and more impressed.

One thing that I don't see here but which I think it might be important is to grab the stack trace in the failure, or atleast the Filename / Line Number of where each Err is allocated. It will be really useful to be able to see the details for figuring out what went wrong, and following the stack trace is very important. With that I think the abstraction is 99%+ superior to exceptions, which is quite a step forward for the programming craft.

What really has to be guarded against though is that a lot of functions are going either have function signature that is much more verbose which leads programmers `cheating` and just `swallowing` the error by forcing the result out -OR- Return type inference might save the day (not sure if Rust has return type inference of not), however it doesn't solve everything, writing the return types on public functions pretty quickly becomes standard operating procedure, and then programmers are going to get lazy again and we are right back at checked exceptions `polluting` their interfaces and they might not want to deal with it.

  interface Perfect {
    def foo : Foo
    def bar : Bar
  }

  interface NotPerfect {
   def foo : Result[Foo, FooErr]
   def bar : Result[Bar, BarErr]
  }
Which of these do I implement first? Does the type system allow me to substitute a Perfect when a NotPerfect is required, or do I have to re-teach it every single time, that in fact, it's totally ok to do so (make a method `perfectCanBeUsedForNotPerfect(p : Perfect) : NotPerfect = new NotPerfect ....)`? Can I fix the issue for a limited set of cases and declare that Perfect implements NotPerfect and is therefore a subtype? Can I do similarly if I implement NotPerfect after Perfect was implemented (add an implicit conversion from Perfect to NotPerfect)? Can I just avoid the whole problem all-together with by-name subtyping (aka Point3{x,y,z} is considered a subtype of Point2{x,y} because Point3 has all the same named fields as Point2 with an extra one added) and informing the type system of the fact that Foo is a valid substitution for Result[Foo,_] because Result[A,B We static programmers will grumble on regardless, we pay our dues to the type checker because we believe its gives us structure and forces failure when while we write the code, not because we want to write boilerplate to teach it simple facts.

...

All in all this is really cool. From a Scala perspective it's a much improved version of Either (and now Try), and it's going to be supported by the standard library in Rust. Which is awesome.

Either itself basically is Scala saying, we don't have type disjuction, so here is a disjunction of 2 types which we will call Left and Right. And by convention Left is used to hold the error condition.

Because no one could remember that Left by convention meant error, and because it would by nice to gussy up what is held in the error type by having your errors be able to point a source error, Scala then later added a Try disjunction. Which is composed of two types Success[A](a:A) and Failure(e : Throwable).

The Success part is fine it's hard to mess that part up, it's just like Ok, having a uniform this just a wrapper around another type would be nice but, we suck that down as programmer business as usual, the Failure side however leaves a lot to be desired, by DESIGN the failure side cannot hold a failure and instead has to hold an exception, this is wrong, failure is a failure. Given the runtime on which Scala runs, the JVM, ignoring exceptions all-together is probably a worse evil. However it should have been made a special subcase of the Failure subclass rather than the only thing it can contain, or Try itself should have been the sub-implemenation of some more generic structure.

What to add a custom message to your failure? Ok make an exception that has a message in it.

Want to pass a Failure up the chain while adding your own error message for context? Ok make an exception then add the failure into it.

Want to fail without an exception? Ok make an exception and put it in the Failure.

Looks like Rust is moving towards a great implementation. This is something that has to be done in the standard library and has to be done well. The more I see of Rust as a language the more impressed I am, when is Rust going to compile to Javascript, oh never I see[1], well back to my ScalaJS cave then :)

1:https://news.ycombinator.com/item?id=4630403

Re: On Error Handling in Rust

#33
post #11

Earlier quoted context omitted.

Doesn't multiple dispatch require run-time type analysis? If so wouldn't that go against Rust's philosophy of zero-cost abstractions?

nope, its all compile time. its just that instead of deciding which impl to pick based upon information X, youre using a pair of pieces of information (X,Y) Edit, also I think the "convertable" class idea is nearly expressible using associated types in rust today,but im not 100% certain about that

Wikipedia implies that multiple dispatch is done by run-time time analysis in OO languages, which does not apply to Haskell or Rust: https://en.wikipedia.org/wiki/Multiple_dispatch

Maybe it's being pedantic, but I can't tell. Are Haskell's type classes strictly as flexible as multiple dispatch?

Re: On Error Handling in Rust

#34
post #31

In some ways it's heartening to see Rust working everything out for itself - but it's also painful to watch the language stumble on the same problems that we've already solved. You're going to hit the same problem again with async, with transactions, and with resource management; indeed some of the stuff I've already seen about borrowing and the like seems achingly close to the same pattern. Introducing new sigils li…

I agree, I could not write it better myself.

Going down one level and finding a better way to express the sugar around `map` and `flatmap` on Monads (and then `withFilter` in Scala) without resorting to compiler magic would be really cool and would let programmers add their own constructs that operate on a lot more than monads.

Out of curiosity, do you know of another language that has something on the level of Result type in its standard library? I haven't seen one. I know Scala has Either/Try, however those are both inferior to Result as a potential Exception replacement.

Re: On Error Handling in Rust

#35
post #31

In some ways it's heartening to see Rust working everything out for itself - but it's also painful to watch the language stumble on the same problems that we've already solved. You're going to hit the same problem again with async, with transactions, and with resource management; indeed some of the stuff I've already seen about borrowing and the like seems achingly close to the same pattern. Introducing new sigils li…

HKT is indeed on the long-term roadmap, but it remains to be seen whether a design can be devised that plays nicely with the fundamental features of Rust (note that the language reserves the unused `do` keyword for just this purpose). Doing this properly is very much research-project territory.

And unless I'm reading the RFC incorrectly, I think this is more principled than you're making it out to be. `FromError` is not magic or specially-handled by the compiler in any way, it's just a trait defined in the stdlib. The `try!` macro isn't calling any magic methods, it's just expanding to a pattern match that itself makes use of typeclasses. It's indeed true that this would only work with variants of the `Result` type, but I don't think that's especially heinous (users can easily supply their own specialized versions of this type, and almost always do). And if the `?` syntax is accepted, it will be able to be used with any type that implements the `Carrier` trait (which is sorta specially-treated by the compiler, though users can still override it via lang items), and would replace the `try! macro entirely. Nothing here is ad-hoc.

Finally, even if Rust had HKTs, I could be convinced that error handling in particular is important enough to require a dedicated syntax to set it apart.

Re: On Error Handling in Rust

#36
post #22

Earlier quoted context omitted.

Maybe I am missing some subtleity here, but isn't the whole point of the described proposed feature that you would be able to replicate the Python code pretty much exactly? fn read_database() -> Result { let db = pgsql.connect(localhost)? let stmt = db.prepare("INSERT INTO log VALUES(?,?)")? stmt.execute(("debug", "Note to self: debug logs are noncritical"))? } So to have the handling code there as well, wrap it in a…

Note my mention of the too-many-small-functions-with-one-caller(tm) smell. It refers to this solution.

I see, yes. Implementation-wise, I don't think this is a problem, the inner function can easily be inlined by the compiler. But language-wise I agree that it is somewhat ugly. Some kind of sugar over this kind of construction would be nice.

Re: On Error Handling in Rust

#37
post #31

In some ways it's heartening to see Rust working everything out for itself - but it's also painful to watch the language stumble on the same problems that we've already solved. You're going to hit the same problem again with async, with transactions, and with resource management; indeed some of the stuff I've already seen about borrowing and the like seems achingly close to the same pattern. Introducing new sigils li…

I have the same concern, but I'm not sure how efficiently you could get monads to compile. It would be possible (but tricky) to come up with a macro for haskell's do syntax that would translate

    monad!(
      sock 
to

    TcpStream::connect(host, port).map(|sock| {
      let parser = Parser::new(&mut sock as &mut Reader);
      parser.parse_value()
    })
but could you get that to compile to the same machine code as this?

    match TcpStream::connect(host, port) {
      Ok(stream) => {
        let parser = Parser::new(&mut sock as &mut Reader);
        parser.parse_value()
      },
      Error(e) => { return e; }
    }
Rust has the same "don't pay for what you don't use" mantra as C++, and introducing the overhead of stack closure in the canonical error handling method would be unacceptable. It might be possible to optimize monadic code as nicely as e.g. iterators, but I'm not convinced.

Re: On Error Handling in Rust

#38
post #27

This is a sugar-coated version of errors as return values. It fixes the most glaring problem, which is code that does not check for errors, common in C: FILE* f = fopen("somefile", "r"); fwrite("aha", 4, 1, f); // In Rust (pardon my rust, I'm totally ignorant), it'd be: let f = fopen("somefile", "r"); fwrite("aha", f)?; The compiler would know that fopen may return an error, and forbid me from running unchecked code.…

> This is a sugar-coated version of errors as return values. By which you mean that errors as return values are bad, and that they are trying to sugar-coat the fact that they are using errors as return values?

No, I do not mean that. I mean it is language syntax to aid in using return values for error signaling. There is no intended negative load, although, in retrospect, using "sugar-coated" as "coated in syntactic sugar" may have introduced an unintended negative meaning.

Re: On Error Handling in Rust

#39
post #36

Earlier quoted context omitted.

Note my mention of the too-many-small-functions-with-one-caller(tm) smell. It refers to this solution.

I see, yes. Implementation-wise, I don't think this is a problem, the inner function can easily be inlined by the compiler. But language-wise I agree that it is somewhat ugly. Some kind of sugar over this kind of construction would be nice.

I was not thinking in terms of compilation result. My problem with this solution is readability. Functions get extracted in the name of reusability. When they are called just once, they do not serve that purpose, and just use space in the programmers mental map of the program.

However, I imagine your type of solution could be used in a macro, like jeremyjh suggested in a parallel comment, producing simple code and the expected functionality.

Re: On Error Handling in Rust

#40
post #22

Earlier quoted context omitted.

Maybe I am missing some subtleity here, but isn't the whole point of the described proposed feature that you would be able to replicate the Python code pretty much exactly? fn read_database() -> Result { let db = pgsql.connect(localhost)? let stmt = db.prepare("INSERT INTO log VALUES(?,?)")? stmt.execute(("debug", "Note to self: debug logs are noncritical"))? } So to have the handling code there as well, wrap it in a…

Note my mention of the too-many-small-functions-with-one-caller(tm) smell. It refers to this solution.

It would be inlined by the compiler, and the standard library could introduce a macro to eliminate the awkward code.

    try!({
      let db = pgsql.connect(localhost)?
      let stmt = db.prepare("INSERT INTO log VALUES(?,?)")?
      stmt.execute(("debug", "Note to self: debug logs are noncritical"))?
    } catch err: DatabaseError {
      println!("Could not write log to database {}", err);
    })
===>

    match (|| {
      let db = pgsql.connect(localhost)?
      let stmt = db.prepare("INSERT INTO log VALUES(?,?)")?
      stmt.execute(("debug", "Note to self: debug logs are noncritical"))?
    }) {
      Document(d) => d,
      DatabaseError(err) => println!("Could not write log to database {}", err)
    }
Post reply on HN