Live data from Hacker News

On Error Handling in Rust

lucumr.pocoo.org

61–70 of 82 posts

Re: On Error Handling in Rust

#61
post #48

Earlier quoted context omitted.

Funnily enough, we actually used to have both Either and Result, until one day we went through and realized that no code in existence was using Either and decided to go all-in on Result instead.

Oh ok that makes sense. Either has a little more to it, as I am sure you are aware, specifically Type Disjunction, but a crappy version of Result is all anyone ever really uses it for as far as I have seen. Real quick for those that don't understand, when I say Type Disjunctions (aka union types) I mean a type which has a value which the type system is guaranteeing is 1 of X different types. So Either[String,Int] is…

Rust has always supported type disjunctions :)

Result is simply defined as

    pub enum Result {
        Ok(T),
        Err(E),
    }
where enum is a general 'variant type' mechanism.

(Nor is Rust a fan of type hierarchies in general - it doesn't even have subclassing.)

Re: On Error Handling in Rust

#62
post #59

Earlier quoted context omitted.

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 Resul…

Is Result not just a specialised Either? In what way is Either inferior?

> Is Result not just a specialised Either? In what way is Either inferior?

It's better named when it comes to being a value/error union: Result/Ok/Err is somewhat more obvious than Either/Right/Left, especially for people who don't make the connection between "right" and "correct".

Aside from that, Result was annotated with #[must_use], so the compiler will warn if you ignore a Result return value:

    fn main() {
        f1();
    }

    fn f1() -> Result {
        Ok(())
    }
=>

    > rustc test.rs
    test.rs:2:5: 2:10 warning: unused result which must be used, #[warn(unused_must_use)] on by default
    test.rs:2     f1();
                  ^~~~~
doing that with Either would be weird.

Those are not huge, but they're small tweaks specially making Result a better fit for an exception replacement.

Re: On Error Handling in Rust

#63
post #59

Earlier quoted context omitted.

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 Resul…

Is Result not just a specialised Either? In what way is Either inferior?

I agree 100% that it is a specialized either.

I would argue that either is inferior because programmers empirically won't be bothered remembering that Left = Err and Right = Ok. Also fixing your Left to conform to a common error type which supports chaining is directly is also pretty important. This can be accomplished in a few different ways, but it's going to be done so often that Either ends up never being used for anything other than Results base class / Result is implemented as a type-class on either, with crappy names for error and ok.

Overally though I dislike Either, because to me, it is a symptom of a language lacking a way to declare anonymous type disjunctions inline[1], just like things like std::pair indicates a language lacks tuples.

1: Something like

def parseToken : ParseError | Int | String

looking at that maybe with inline type disjunctions the need for Result itself melts away.

Re: On Error Handling in Rust

#64
post #61

Earlier quoted context omitted.

Oh ok that makes sense. Either has a little more to it, as I am sure you are aware, specifically Type Disjunction, but a crappy version of Result is all anyone ever really uses it for as far as I have seen. Real quick for those that don't understand, when I say Type Disjunctions (aka union types) I mean a type which has a value which the type system is guaranteeing is 1 of X different types. So Either[String,Int] is…

Rust has always supported type disjunctions :) Result is simply defined as pub enum Result { Ok(T), Err(E), } where enum is a general 'variant type' mechanism. (Nor is Rust a fan of type hierarchies in general - it doesn't even have subclassing.)

Oh well I guess I will have to read up on rust some more then before writing long winded posts :) That's very cool.

Does it allow inline type disjunction declarations?

Rather than (never written rust before) something like:

  pub enum ParseResult {
    ParseError(Err) 
    IntResult(Int)
    StringResult(String)
  }
  def parse : ParseResult
Allowing something like this

  newtype ParseError = Err
  def parse : ParseError | Int | String = ...
This avoids having to create specific names for each different type in the disjunction.

I mean we could implement tuples like this

  pub tuple MapEntry {
    Key(K)
    Value(V)
  }
  class SortedMap {
    def firstEntry() : Option>
  }
but everyone probably agrees, that we can figure out that key's are first and values are second, so let's just do this:

  class Map {
    def firstEntry() : Option
  }

Re: On Error Handling in Rust

#65
post #61

Earlier quoted context omitted.

Rust has always supported type disjunctions :) Result is simply defined as pub enum Result { Ok(T), Err(E), } where enum is a general 'variant type' mechanism. (Nor is Rust a fan of type hierarchies in general - it doesn't even have subclassing.)

Oh well I guess I will have to read up on rust some more then before writing long winded posts :) That's very cool. Does it allow inline type disjunction declarations? Rather than (never written rust before) something like: pub enum ParseResult { ParseError(Err) IntResult(Int) StringResult(String) } def parse : ParseResult Allowing something like this newtype ParseError = Err def parse : ParseError | Int | String = .…

No, it doesn't. I think this is the right choice, because when unpacking you need some way to distinguish them anyway, i.e. in

    match foo {
        ParseError(e) => ...
        IntResult(i) => ...
        StringResult(s) => ...
    }
you need something adorning the left to determine what 'e', 'i', and 's' are; you could use the type, but compared to that it doesn't save much typing to just name the branches (which can always be abbreviated), which avoids issues with multiple variants that happen to have the same type.

Re: On Error Handling in Rust

#66
post #65

Earlier quoted context omitted.

Oh well I guess I will have to read up on rust some more then before writing long winded posts :) That's very cool. Does it allow inline type disjunction declarations? Rather than (never written rust before) something like: pub enum ParseResult { ParseError(Err) IntResult(Int) StringResult(String) } def parse : ParseResult Allowing something like this newtype ParseError = Err def parse : ParseError | Int | String = .…

No, it doesn't. I think this is the right choice, because when unpacking you need some way to distinguish them anyway, i.e. in match foo { ParseError(e) => ... IntResult(i) => ... StringResult(s) => ... } you need something adorning the left to determine what 'e', 'i', and 's' are; you could use the type, but compared to that it doesn't save much typing to just name the branches (which can always be abbreviated), whi…

To be fair, a similar argument also applies to structs vs. tuples. Unpacking them is really awkward. I suspect language designers only tolerate them because they're so convenient in practice for representing mathematical tuples (where order is actually semantically significant) and have very lightweight syntax in cases where you want to use all or most of the values. But with variants, order is never meaningful and you can only have one disjunctive type at at time (which quashes both those points). There's a Rust RFC which proposes some rather icky syntax for them (match foo { (e|!|!) => ... | (!|i|!) => ... }) and that alone convinced me that this is a no-go.

Re: On Error Handling in Rust

#67
post #18
post #3

Wow, I really, really like that `?` operator idea. I haven't quite dived into Rust yet (I'm watching and waiting for v1.0), but I would be really excited to work with a language that makes error handing so easy and safe.

You could try Haskell.

I have. I've learned a lot, but I just don't have any projects right now that make Haskell a great. That isn't to say I couldn't write them in Haskell, but my current projects are writing code I'm unfamiliar with, so I don't want the double whammy of struggling with the logic and structure AND learning the language.

Re: On Error Handling in Rust

#68
post #65

Earlier quoted context omitted.

Oh well I guess I will have to read up on rust some more then before writing long winded posts :) That's very cool. Does it allow inline type disjunction declarations? Rather than (never written rust before) something like: pub enum ParseResult { ParseError(Err) IntResult(Int) StringResult(String) } def parse : ParseResult Allowing something like this newtype ParseError = Err def parse : ParseError | Int | String = .…

No, it doesn't. I think this is the right choice, because when unpacking you need some way to distinguish them anyway, i.e. in match foo { ParseError(e) => ... IntResult(i) => ... StringResult(s) => ... } you need something adorning the left to determine what 'e', 'i', and 's' are; you could use the type, but compared to that it doesn't save much typing to just name the branches (which can always be abbreviated), whi…

Yep, the multiple same type issue definitely happens and that compicates client side matching. In my experience it has been infrequent enough that having to make the a couple wrapper classes would be preferable. Sometime tuples can be very ambiguous, take points for example. Point(x:Int,y:Int) is similar to (Int,Int), however sometimes the anonymity is nice so you will want to have both options.

The Boilerplate grows really fast as you try to pass results up a call hierachy.

So let me extend the example to demonstrate it how it doesn't scale:

  //Sorry for the Scala-ness
  //Presume a mapByType partial function on all discriminated unions if the union value is of that type, then it calls
  //the partial function otherwise it just returns whatever it's current value is
  def lexAndParse : ParseError | LexError | Int | String = lex().mapByType{ case t : Token => parse(t) }
  newtype LexError = Err
  def lex() : LexError | Token = ...

  newtype ParseError = Err
  def parse(t : Token) :  ParseError | Int | String = {
      tryParseInt(t).orElse(tryParseString(t)).getOrElse(ParseError("$t not Int Or String"))) }
    }
  def tryParseInt(token : Token) : Option[Int] = ...
  def tryParseString(token : Token) : Option[String] = ...

versus:

  pub enum LexParseResult {
    LexError(Err) 
    ParseError(Err) 
    IntResult(Int)
    StringResult(String)
  }
  def lexAndParse : LexAndParseResult = {
    match lex() {
      LexResult.LexError(e) => LexAndParseResult.LexError(e)
      LexResult.TokenResult(t) => match parse(t) {
         ParseResult.ParseError(e) => LexParseResult.ParseError(e)
         ParseResult.IntResult(e) => LexParseResult.IntResult(e)
         ParseResult.StringResult(e) => LexParseResult.StringResult(e)
      }
    }
  }

  pub enum LexResult {
    LexError(Err) 
    TokenResult(Token)
  }
  def lex : LexResult

  pub enum ParseResult {
    ParseError(Err) 
    IntResult(Int)
    StringResult(String)
  }
  def parse(token : Token) : ParseResult = {
     match lex(){
       Token(t) => 
        tryParseInt(t).map(r=>ParseResult.IntResult(r)).orElse(tryParseString.map(r=>ParseResult.StringResult(r) )).getOrElse(ParseResult.ParseError("$t not Int Or String"))) }
       LexError(e) =>  ParseResult.LexError(e)
    }
  }
  def tryParseInt(token : Token) : Option[Int] = ...
  def tryParseString(token : Token) : Option[String] = ...

Re: On Error Handling in Rust

#69
post #65

Earlier quoted context omitted.

No, it doesn't. I think this is the right choice, because when unpacking you need some way to distinguish them anyway, i.e. in match foo { ParseError(e) => ... IntResult(i) => ... StringResult(s) => ... } you need something adorning the left to determine what 'e', 'i', and 's' are; you could use the type, but compared to that it doesn't save much typing to just name the branches (which can always be abbreviated), whi…

To be fair, a similar argument also applies to structs vs. tuples. Unpacking them is really awkward. I suspect language designers only tolerate them because they're so convenient in practice for representing mathematical tuples (where order is actually semantically significant) and have very lightweight syntax in cases where you want to use all or most of the values. But with variants, order is never meaningful and y…

Order is important in rust variants? That sucks, hmm I hadn't thought about it in this much depth and it's probably obvious from literature seems like there is a whole spectrum of variants then:

1. wrapper variants (where the choosen instance is given a key or it's own wrapper type)

2. ordered variants (where the choosen instance if keyed by it's position)

3. type variants (The only way to differentiate is by type, duplicate types collapse into one type)

I obviously prefer type variants where:

Int | Int | Int simplifies to Int

I think it gives the typechecker the ability to make and understand a much larger variety of useful properties.

For example a method that takes

  def print(x: Num | String)
it will accept Int | String or String | Int or String | Double or String or Int or Double

obviously that isn't totally ideal, you probably rather use polymorphism. (I think I read that the Ceylon implementors thought it would be a big win here then they preferred to use polymorphism, but I could be spreading FUD against my own postion :)

BTW I would argue that type variants have the opposite property of tuples in that as they grow longer that make code much more comprehensible.

Re: On Error Handling in Rust

#70
post #58
post #57

Earlier quoted context omitted.

`Carrier` is still pretty parochial; it has this normal/exception distinction hardwired into it, no? The RFC explicitly rules out using Vector with it, and it doesn't look possible to implement for async constructs, or STM transactions, or the like? I'd very much like to be wrong here. FromError is not handled specially by the compiler, but it is handled specially by the try macro; does the signature of try make the…

> `Carrier` is still pretty parochial; it has this > normal/exception distinction hardwired into it, no? To reiterate my earlier point, I'm personally fine with the existence of an entirely separate mechanism for error handling. Mind you, not that this invalidates your desire for a more general mechanism for async et al. Until/if we get HKTs, we'll probably continue to achieve this with bespoke macros as per today's…

> The `try!` macro is just as non-special as `FromError` (and `Result` and `Option`). You're free to recreate the whole ecosystem in your own libs if you'd like (ignoring the `Carrier` proposal for the moment and its associated syntax).

Sure. I'm coming at this from a viewpoint of a) syntax is very important b) user-defined macros are generally undesirable

Post reply on HN