Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

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 <: Err] is a type-disjunction of A | B and type-disjunction allows for substitution by the disjoint types? Where is Rust going to fall on this continuum and how many programmer hours is going to waste for each and every one of them to teach the its type-system over and over and over these simple patterns via boilerplate code?

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



  > not sure if Rust has return type inference of not
Rust has return type inference for function invocations, but not for function signatures. This is a conscious decision to force all functions to provide an explicit contract rather than having any global type inference. And really, I wouldn't want function return types to be completely inferred in Rust given its last-semicolon rule.

However, there is an RFC that would allow for "abstract return types", which would let a function state that its return type implements a certain trait and allow the programmer to omit the concrete type. This is somewhat essential for the new closure design, and also greatly benefits functions that return iterators (but it might not be relevant to your question at all, I haven't had my tea yet...).

https://github.com/rust-lang/rfcs/pull/105/files

  > when is Rust going to compile to Javascript
Never officially, but that doesn't mean an Emscripten backend isn't possible https://github.com/rust-lang/rust/issues/2235 :)


Rust used to also have Either, and it was changed to Result for the same reasons. Name things what they mean. :)


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.


Someday, we should have a "History of Rust" thing, kinda like folklore.org or something.


I'd love to compile such a thing, but doing it properly would involve both cataloguing every change and recording the rationale for each. And because the rationale for changes is so dependent on the context of the-language-as-it-was-at-that-moment-in-time, random access of the archive would leave your understanding incomplete. For example, I just said that we removed `Either` because nobody was using it and because it was structurally a complete duplicate of `Result`... but I omitted the fact that `Result` was almost universally shunned at the time as well, in favor of `Option`. :P

Part of me wonders if the best way to do it wouldn't be to to start at Rust 1.0 and go on a tour backwards through history, i.e. "here's a great language, now let's all make terrible decisions to make it worse!" :)


Stroustrup’s The Design and Evolution of C++ is a fascinating case study of trying to create a good programming language in the real world. I suspect that, once Rust has stabilised and the dust has settled, a retrospective from key contributors in a similar style would also make very interesting reading. I’m sure I’m not the only one following along and looking forward to 1.0 but not necessarily keeping up with the day-to-day decisions and all the detailed debate as the language evolves.


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 a specialized case where X=2 and the value is either a String or an Int. This ends up being really similar to Tuples, Tuples are often given their own syntax, typically, (A,B,...) and frequently in languages that don't support the tuple abstraction natively, things like std::pair<A,B> pop up as a poor substitute special cased to X=2 element tuples.

This leads to my view that Either is to Type Disjunctions as something like std::pair is to Tuple, scratching the surface of a deeper and much richer abstraction. When it pops up in your language its a symptom of lacking type unions, just as std::pair is a symptom of lacking tuples.

Do you think Rust will ever have native support for type disjunctions?

I understand that it probably principly uses object hierarchys for this type of stuff, and in a lot of ways that makes a lot of sense, but often time when writing statically typed message passing code, aka Consumers / Producers, the more ad hoc representation + syntax support for unions eliminates huge traunches of boilerplate and provides static guarantees that all the cases are handled consider this code for example.

As real world, recurring pain in the neck example, in Scala to preserve type safety and exhaustiveness checking at my company we create a sealed trait hierarchy where each subclass wraps each different type in the disjunction.

It's a poor man's type disjunction and it takes A LOT of boilerplate.

Its still worth it to jump through these hoops because in a message passing system built around producers/consumers(aka, sources/sinks, emitters/handlers etc) we get the following important benefits:

a. Only messages that fall in a set of allowed types can be passed into a handler, guaranteeing that a message the handler can't handle won't be passed to it.

b. A handler is typically backed by a match expression which handles the different subtypes the handler handles. We used a sealed trait in scala pattern which guarantees that if someone adds another type to the hierarchy without adding it to all the handlers (which might be in different codebases) there is a compile error for a non-exhaustive match in the handler's implementation instead of a runtime blowup there when the unhandled type falls through the match.

Obviously this is just scratching the surface of the benefits that type disjunction can provide. Hopefully enough programmers and language designers will have an 'aha moment' and realize that just as adding lambda's to abbreviate anonymous functions opens up the world of higher order functions, and tuples just eliminate doing the same thing thousands of crappy different ways, and having an Option (aka Maybe) type eliminates null pointer exceptions, and hopefully standard library sanctioned Result type eliminates unhandled exceptions, having language support for type disjunctions will have benefits similar to all these other `essential` features.

TL;DR Type Disjunctions are really useful, and are missing fundamental in most type systems do you think Rust will support them?


Rust has always supported type disjunctions :)

Result is simply defined as

    pub enum Result<T, E> {
        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<K,V> {
    Key(K)
    Value(V)
  }
  class SortedMap<K,V> {
    def firstEntry() : Option<MapEntry<K,V>>
  }
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<K,V> {
    def firstEntry() : Option<(K,V)>
  }


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.


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] = ...


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.


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.


Order isn't currently significant; it's the RFC (which I haven't seen) which proposed adding this in some form.


Wow, I just looked over some of them and I think it's awesome that Rust has these RFCs tied to pull requests. Scala has SIPs but they are much less frequently used, and their granularity is much more coarse, yet they are less detailed and commented on by the community.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: