Hacker News new | ask | show | jobs
by ww520 3400 days ago
Speaking of removing friction, there are three areas that have caused me grief when I wrote Rust code:

1. Error handling. The lack of built-in support for multi-error or error union in Result is painful in dealing with different types of error in a function. Support for Result<Value, Error1 | Error2 | Error3> would be helpful. Or may be support for easily converting one type of error to another. Now there's lots of boiler plate code to deal with error conversion. Error chaining would be nice, too.

2. Lack of stack trace when an error occurs. Now that stacktrace starts when panic!() is called, which is kind of late.

3. Better support for conversion between &str and String. Dealing with strings is so prevalent in programming that making it easier to work with the two types would be a huge boost to productivity.

Edit: another item

4. Support of partially applied function , i.e. bind a subset of arguments to the function pointer. Currently there's no way to bind the self argument to the Option/Result chaining calls. Basically the Option/Result chain (.and_then, .map, etc) only carries forward the value of Option/Result and nothing else. It would be nice put partially applied function in the chain. e.g. result.and_then(self.func1) where func1 has the self argument bounded. Or in more general form, result.and_then(func1("param1", param2, _)) where func1's first and second parameters have been bounded up front and the value of result will be passed in as the 3rd parameter.

4 comments

> 1. Error handling. The lack of built-in support for multi-error or error union in Result is painful in dealing with different types of error in a function. Support for Result<Value, Error1 | Error2 | Error3> would be helpful. Or may be support for easily converting one type of error to another. Now there's lots of boiler plate code to deal with error conversion. Error chaining would be nice, too.

There are a couple of crates that support this; personally, I recommend the "error-chain" crate. However, I do wish that Rust promoted the most capable of those to the standard library.

> 2. Lack of stack trace when an error occurs. Now that stacktrace starts when panic!() is called, which is kind of late.

error-chain provides that.

> 3. Better support for conversion between &str and String. Dealing with strings is so prevalent in programming that making it easier to work with the two types would be a huge boost to productivity.

Can you give some specific examples of cases you've found cumbersome?

String has a Deref instance for &str, so taking a reference to a String automatically works as a &str. You can also call .as_str().

Going in the other direction, you can call .to_string() to make a copy of a &str as a new String.

1. Some of the 3rd party error chain libraries help somehow but they don't help to remove the boiler plate when dealing with different types of error. I just want to be able to do:

    fn func1() -> Result<MyStruct, Error1 | Error2> {
        let foo = foo()?;  // which can return Error1
        let bar = bar()?;  // which can return Error2
        ...
    }
2. Error chain only shows the stack where I explicitly add it to the chain. Anything underneath is not shown. All these should be done by the runtime instead of forcing the developers to add code to handle it.

Also, error handling is so prevalent in Rust. If error chain is the way to go, make it as built-in. As right now, everyone has to hit a brick wall with Result, and then hunt around for the same solution.

3. It's the other way around &str to String, having to call .to_string() everywhere. Make it implicit and automatic if the type expects a String while a &str is passed in.

I completely agree that Rust ought to build in support for the error-chaining pattern. I think I'd still prefer to have a named type, but the standard library should provide a standard way to construct that type.

> 3. It's the other way around &str to String, having to call .to_string() everywhere. Make it implicit if the type expects a String while a &str is passed in.

You can't turn a reference like &str into an owned type like String without making a copy, and Rust doesn't do implicit copies (among many other reasons, because doing so would make it harder to notice code patterns that will lead to poor performance). So you'll always have to have some explicit indication that you want to make a copy.

In general, most functions should accept &str parameters rather than String, for exactly that reason; you should rarely run into functions that want a String. You can also use the "Cow" type if you want to support both owned and borrowed strings in the same structure.

> I think I'd still prefer to have a named type, but the standard library should provide a standard way to construct that type.

Having ad-hoc error union Result per function make it lightweight, and less friction in writing code. I would go one step further, let the compiler build the error union automatically.

    fn func1() -> Result<MyStruct, _> {
        let foo = foo()?;   // might return Error1
        let bar = bar()?;   // might return Error2
        ...
    }
The compiler infers the list of possible error types returning from the functions. Func1() would automatically have the return signature of Result<MyStruct, Error1 | Error2>.

Make the type inference to good use.

That kind of automatic sum-typing seems like an interesting idea. You might consider bringing it to the Rust internals forum, posting it as a pre-RFC, and discussing it as compared to some of the alternatives. That might lead to either a change in the direction you hope for, or the unearthing of other ergonomic approaches.
This idea has been waved around a bit, but in the form of `impl Error` where `_` is. That is, inside the function the `E1 | E2 | ...` type is being built, and if you have automatic dispatch for `Error`'s methods then it will work with `impl Error`.

Actual global inference has never been on the table and still isn't.

I believe there are conversations of being able to promote enum variants to full-blown types, though I don't know if there's any project for lightweight enums of types.
I believe inferring sum types like that would make the inference system far more sensitive and complicated and possibly even slower. For one, it would likely mean mistakes like, say, assigning two different types to a variable may result in weird error messages, and may also limit how often coercions trigger.
I seem to run into functions that require String instead of &str all the time, most recently the "assert_eq!" macro which -- maybe I'm not understanding it correctly -- refuses to compare a &str to a String.

Can you tell me how I would remove the excessive uses of ".to_string()" from the tests in this module? https://github.com/rspeer/rust-nlp-tools/blob/master/languag...

  fn main() {
      let s1 = "Hello, world"; // &str
      let s2 = String::from("Hello, world"); // String
      
      assert_eq!(s1, s2);
      assert_eq!(s2, s1);
  }
compiles just fine for me?
Okay, once again I try to find an example from my code, and once again it turns out that Rust makes it simple in the simple case, but the more complex cases are still confusing.

If I change the test value Some("zh".to_string()) into Some("zh"), it points to that line and tells me:

    expected struct `std::string::String`, found &str
Sure, it's a different situation because the value is wrapped in an Option. But if String and &str are truly compatible, I would never expect to see that error message.

    -> Result<MyStruct, Box<Error>>
accepts almost any type of error (via dynamic dispatch of things that implement std::error::Error trait).
That's a good idea. It's the shotgun approach.
> String has a Deref instance for &str, so taking a reference to a String automatically works as a &str. You can also call .as_str(). > > Going in the other direction, you can call .to_string() to make a copy of a &str as a new String.

As a beginner in Rust myself, I would like to observe that &str vs. String problems come up all the time for me. Every one of them is quickly resolved by knowing where to add an & or a method call, it seems, and experts know when to do this, and they stop noticing the problem because it's only the beginners who are doing it wrong.

But when every beginner is doing it wrong and every expert isn't, there is an ergonomics problem.

I think part of the problem might be that String and &str are not related in any intuitive way. My initial work in Rust was littered with what the heck is this, what's a str that there's a reference to, etc.

I understand now, but initially it made absolutely no sense that String is a heap-allocated owned string and &str is a reference to a chunk of string data of known length stored somewhere that I probably don't have to care about.

It may be a teaching thing, but I do wonder if they could have been named better. I just don't really know what else you'd call them without them becoming overly verbose.

I often joke that String -> StrBuf is my #1 wishlist item for a theoretical Rust 2.0.
I absolutely agree with you, and it seems worth looking closely at the problem to see if some change would make it easier to learn. However, the ownership and borrowing system represents the single biggest innovation and key idea of Rust, as well as the thing with the least ability to apply learnings from other languages. So while we should look closely at any roadblocks that make it harder to learn than necessary, we can't make it entirely transparent.
This is one reason why the new book focuses on teaching ownership and borrowing with String and &str. You're 10)% right.
> Support for Result<Value, Error1 | Error2 | Error3> would be helpful. Or may be support for easily converting one type of error to another.

It sounds like you want to create a new error type that's an enum of Error1, Error2, Error3 and then just implement the From traits. Then you can use ? with no boilerplate error handling.

I'm sure you've already considered this though. Why wouldn't that work for you?

Declaring the new enum and implementing it are the boilerplate.
Lots of typing and mental overload. Since we are talking about ergonomics and reducing friction, it would be good to make Result handling easier.
The From trait can be derived using my derive_more crate. That would at least decrease the boilerplate a bit.

http://jeltef.github.io/derive_more/derive_more/

I don't understand there are no stack traces unless you use a library?
for 1. there's map_err(..) which accepts a closure

e.g.

`try!(foo().map_err(|e| format!("{}", e.thing)));`

That's what I'm doing right now all over the place and I hate the boiler plate.
ah, fair enough!