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

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.



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.


`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 relationship between the two more obvious? I haven't been following the state of Rust IDEs, but I'd hope that they can make it more obvious which typeclasses are involved than is clear from the text of the code (apologies for this awful sentence).

FWIW doing things this way doesn't rule out a dedicated syntax on top of it - see the recent SIP for async/await in scala.


  > `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 `try!`.

  > FromError is not handled specially by the compiler, but it is handled specially by 
  > the try macro
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).


> 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


> (note that the language reserves the unused `do` keyword for just this purpose).

I'm not entirely sure it's just for this purpose. `do` used to mean something in Rust, but when that syntax was removed, the keyword just wasn't freed up. Of course, if we do gain HKT, it will be nice to have it, but I'm not sure that was the justification at the time.


Removing the old notation was certainly not solely motivated by wanting to free up the keyword, but the reason that it remains reserved is in anticipation of future use, rather than simple negligence (though of course this decision could be reversed before 1.0).


Right, I guess I meant that it's not just for HKT, but for something useful.

Anyway, none of this particularly matters.


F# doesn't have HKT, but still has a variant of do syntax via "computation expressions" http://msdn.microsoft.com/en-us/library/dd233182.aspx. It's less elegant than HKT because you have to name the monad used for the expression i.e. io { exprs }, and also involves additional boilerplate code to define them - but accomplishes many of the same objectives.


The problem is that without HKT you can't abstract over these things; you can't write useful functions like "sequence", and so code that uses these expressions becomes a kind of second-class citizen that can't be refactored the way you'd do with normal code.


Of course, HKT is preferable, just in the error reporting context (i.e. this particular example) I'm not sure much the addt'l abstraction (functions like sequence) further solves the problem, seems like the expressions described may be sufficient.

But I agree, in general, you absolutely want HKT for the reasons you mentioned.

Said another way, if HKT in rust is doable, let's do that - but if that turns out not to be the case, there are some nice conpromises such as this example, which I think, at least, is better than the proposed ? Operator, because it is a bit more general/versatile.


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 <= TcpStream::connect(host, port);
      let parser = Parser::new(&mut sock as &mut Reader);
      parser.parse_value()
    )
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.


> but could you get that to compile to the same machine code as this?

Well not exactly because the second snippet is not equivalent to the first one (it doesn't wrap the result of parse_value() back into an Ok(), and thus I guess wouldn't compile), but aside from that yes: Result.map is trivially implemented in terms of match and (almost always) inlined: https://github.com/rust-lang/rust/blob/master/src/libcore/re... and the closure is itself inlined. So the resulting assembly should be the exact same between the two versions


The only difference between the two is inlining the map method, no? This may be the "sufficiently smart compiler" fallacy, but that really doesn't seem very hard.

Certainly I've been very impressed with the performance I've seen from Haskell (and Scala) on real-world problems, though I appreciate that's not quite the same thing, and a more realtime-suitable language that had the same level of expressiveness would definitely be a Good Thing.


> The only difference between the two is inlining the map method, no? This may be the "sufficiently smart compiler" fallacy, but that really doesn't seem very hard.

It doesn't even rely on a smart compiler: https://github.com/rust-lang/rust/blob/master/src/libcore/re...

The #[inline] attribute is defined as a "standard (though very strong) inline hint" (there's also #[inline(never)] and #[inline(always)])


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.


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.


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.


Yeah, sure, a higher kinded type here, syntax sugar for generalized computation side effects there, and before you know it you're neckdeep in monad transformers and your head rapidly decompresses in a combinatorial explosion every time you try to do a new thing and users need to remember seven different type parameters to read a line from stdin. :(




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

Search: