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.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?
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.
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>.
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".
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.
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 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.
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.... I think. I don't remember it that vividly.
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.
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.