Hacker News new | past | comments | ask | show | jobs | submit login

I recently wrote a few projects in Rust (C/C++/Go/JavaScript/Java/Python as background), and very much like the language. My 2 cents from my endeavors with Rust

I felt like all type errors are backwards. That is, "got" was the target you are giving your type to, not the type that you are passing. This may only happen in some cases, but I just started tuning the content of those errors out and instead adjusted randomly until things worked or the message changed.

I was often getting obscure type errors that were not at all related to the issue, and sometimes the compiler just insisted that just one more burrow would do, no matter how many burrows you stack on. This is definitely because I did stupid things, but the compiler messages were only making matters worse.

String vs. str is a pain in the arse. My code was littered with .as_str() and .to_string(). I never had the right one.

Enums are super nice, but it's very annoying that you cannot just use the value as a type. My project had a lot of enums (user-provided query trees), and it was causing a lot of friction.

There are also many trivial tasks where you think "Of course there is an established ecosystem of libraries and frameworks for this", and end up proven wrong. I mostly did find one library for the thing I needed, but often immature. The HTTP server + DB game seems especially poor.

In the end, I had to quit the fun and get work done (and others did not find playing with new tools as fun as I did), so I ported the project to Go and got productive. I took a fraction of the time to write in Go, the libraries are just so much more mature, it performs significantly better than the Rust implementation (probably because of better libraries—definitely not stating that Go is faster than Rust here), compile takes 1 (one) second rather than minnutes, and there is in general just much less friction.

On the flipside, it takes about 2-3 times as much Go than Rust to do the same task, even if it was way easier to write the Go code. The code is also a lot uglier. As an especially bad case, something that was a serde macro call in the Rust version is 150 lines of manually walking maps in the Go version.




> My code was littered with .as_str() and .to_string().

PSA: If you have a variable that's a String, you can easily pass it to anything that expects a &str just by taking a reference to it:

    fn i_take_a_str(x: &str) {}
    let i_am_a_string = "foo".to_string();
    i_take_a_str(&i_am_a_string);
Every variable of type &str is just a reference to a string whose memory lives somewhere else. In the case of string literals, that memory is in the static data section of your binary. In this case, that memory is just in the original allocation of your String.


Ah, I found that out later but had forgotten all about it. :)

I don't remember how I found out, but it seemed oddly magical until I just now read the docs: String implements Deref<Target=str>. Makes more sense now.

I still had a bunch of to_strings()'s, though, as things tended to take String whenever I had &str's. I found this to be a very unexpected nuisance.

EDIT: Maybe I needed as_str() as the & trick doesn't work if the target type cannot be inferred as &str?


FWIW, this is why we go over this stuff in the book now; lots of people struggle with it, it's not just you.

And yeah, Deref doesn't kick in everywhere, so you may need the .as_str() in those situations. It should be the extreme minority case, generally. Same with .to_string(), though moreso. Most stuff should take &str, not String.


It's relatively rare that APIs should be taking ownership of `String`s from you; the majority of the time arguments should be borrowed. I'm curious what cases you ran into most frequently that required `String`.


as things tended to take String whenever I had &str's

Functions should prefer &str or perhaps T where T: AsRef<str>. Note that if you write code that needs an owned String, you could consider taking some T where T: Into<String>, because this allows you to take many kinds of string types, such as &str, String, Box<str>, and Cow<'a, str>.


I don't remember the details, but I just recall that I ended up with a converting nightmare.

Your suggestions make sense, but I can't help but think that there is something fundamentally weird about basically having to use generic programming just to take a string arg. The most sensible thing would be "everything uses &str".


Your suggestions make sense, but I can't help but think that there is something fundamentally weird about basically having to use generic programming just to take a string arg.

Indeed, it's the hard trade-off to make every time need a string reference, do you want a function that is slightly more general or one that has an easy-to-read signature?

It becomes even more interesting when defining trait methods. E.g.:

    trait Hello {
      fn hello<S>(who: S) where S: AsRef<str>;
    }
Has the benefit of being more general, but cannot be used as a trait object (dynamic dispatch), since it wouldn't be possible to generate a vtable for &Hello.

I generally prefer the generic approach for functions/methods that want to own a String, since using some_str.to_owned() to pass something to a method is not very ergonomic (relative compared to &owned_string for a function that takes &str). But you have to be certain that you don't want to use the trait as a trait object.


It's just a tradeoff, like any other. If you use a generic, you add complexity, but can accept a wider set of types. If you don't, things are simpler, but you accept a smaller set of types. I personally find the AsRef to almost always be overkill. YMMV.


> Enums are super nice, but it's very annoying that you cannot just use the value as a type. My project had a lot of enums (user-provided query trees), and it was causing a lot of friction.

Note that OCaml has this feature of using-a-enum-value-as-a-type (or using a subset of enum values as a type, etc.). It works very well, but it quickly produces impossibly complicated error messages.

I'd like Rust to have this feature, eventually, but not before there is a good story for error messages.


I am not very familiar with OCaml, but why would such a feature result in complicated error messages? I can only really imagine two new error scenarios: "Expected Enum::ValueA, got Enum" and "Expected Enum::ValueA, got Enum::ValueB".


I’m interested in this feature because of the ”incremental” static case analysis it enables. What’s the problem with the error messages?


Oh, and added thing that bugged me a lot: The error part of Result<T, E>. During my short time of coding Rust (I'll get back to it later), I never really found a way to ergonomically handle errors.

I find it really awkward that the error is a concrete type, making it so that you must convert all errors to "rethrow". Go's error interface, and even exception inheritance seems to have lower friction than this.


I'm quite fond of error-chain (https://github.com/rust-lang-nursery/error-chain), which helps mitigate it somewhat. You can do things like:

    use error::{Error, ErrorKind, Result, ResultExt};
    
    fn some_func(v: &str) -> Result<u32> {
        v.parse::<u32>().chain_err(|| ErrorKind::ParseIntError)
    }
The purpose of `chain_err` here is to add on top of the previous error, to explain what you were trying to do, instead of passing up the previous error (in this case, `std::num::ParseIntError`).

If you don't like that, you can do something like this:

    use std::boxed::Box;
    use std::error::Error;

    fn some_func(v: &str) -> Result<u32, Box<Error>> {
        v.parse::<u32>().map_err(|e| Box::new(e))
    }
But then you'd have to box every error.


? should help a lot with this, but libraries like error-chain, or the newer (and, IMO much better) failure can help even more.


But ? only worked if the function it was used in had the same error type as the function you called, which was mostly not the case...

... I think. I don't remember it that vividly.


It doesn't have to be the same type, there just has to be a suitable implementation of the 'From' trait to perform the conversion if the types don't match.


? calls a conversion function. It only won't work if the error you're trying to return won't convert to the error that's in the type signature. Even then, you have options, like .map_err.


We have gone a bit down a tangent here. My original complaint was about the friction of different error types compared to other languages (like, say, Go).

Having to implement a bunch of From traits (unless you need io::Error, because everything seemed to have conversions from/to that), or having to implement inline error conversion through map_err, is such friction. I might go as far as consider it the most cumbersome error system I have used. Clean idea, cumbersome implementation.

My comment about '?' not working with mismatched types was mostly just to say that it doesn't fix anything, it just adds a bit of convenient syntactic sugar.


There is zero friction if you use either error_chain/failure crate and you can still recover the precise underlying errors. The only thing is it allocates.


I will look into those crates next time!




Consider applying for YC's Spring batch! Applications are open till Feb 11.

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

Search: