I see a lot of folks talking about this, but I live in a Rust bubble. From my POV a lot of the folks using Rust use it for primary reasons other than memory safety; many could afford to use python or something and get away with it. I suspect there are fewer blog posts about this, however.
I'll take a shot at some of the advantages:
Algebraic datatypes: Seriously. These are amazing. C++ has them with Boost's variant (and now std::variant), but they're awkward to use, making them somewhat a niche instead of the very central place they take in Rust programming. Till now ADTs were mostly the domain of functional languages, so I'm very happy that Swift and Rust giving them first class support and making them essential.
ADTs in Rust work with the enum keyword:
enum Shape {
Rectangle(Point, Point),
Circle(Point, u32)
}
let some_shape = Circle(Point::new(1, 2), 5);
match some_shape { // like switch, but with pattern matching
Rectangle(p1, p2) => println!("Rect from {} to {}", p1, p2),
Circle(p, radius) => println!("Circle at {} with radius {}", p, r)
}
An enum is essentially an "or" type; i.e. "a Shape is a Rectangle (X)OR a Circle". You are forced to handle this fact when you access this data -- if you try to access the stuff inside some_shape, you
must match (or use a method that does this for you), and the match
must be exhaustive (cover all cases). This is how null works in Rust -- if you want to say something is nullable, you use `Option` -- `Some(T)` for when it's there and `None` for when it's null. You're forced to check for the none case if you want to be able to get to the inner data. You can call `.unwrap()`, but internally that's a method that just does a match and panics in the error case.
Traits: I mention this elsewhere in the thread, but Rust metaprogramming via generics cannot and will never beat C++ TMP's level of expressivity. Rust is going to get procedural macros which make it easier to fill in that gap of metaprogramming in a less hacky way, but if you just compare generics and templates then templates can do a lot more. However, that's not necessarily a good thing. The way templates are structured; they basically get "monomorphized" at something akin to parse time. This means that the error messages can be atrocious, and an API can never be self-documenting since you have to explain what kinds of types can be passed in. On the other hand, in Rust, if you get a function from a library you can pretend that it's a black box. The error messages will never mention the contents of the box. You can adequately figure out how to not make the compiler error out by looking at the type signature. The way Rust traits work is that you first define them:
trait Frob {
fn frob(&self, frobbee: &str);
}
Then you implement them:
impl Frob for SomeType {
fn from(&self, frobbee: &str) { println!("{} frobs {}", self, frobbee) }
}
Now, you can write functions (or types, or methods, or whatever) which accept these:
fn frob_something_10x(frobber: T, frobbee: &str) {
for _ in 1..10 {
frobber.frob(frobbee)
}
}
If I did not have the `T: Frob` bound, I would not be allowed to write this method; the compiler would say that type `T` doesn't have a frob method, and I'd be forced to add a bound that gives it one. If a library user passed the wrong type to the function, the compiler will tell them that their type doesn't implement Frob, at which point they can add an implementation or use a different type or whatever. It's a very nice, clear API separation which leads to clear error messages and makes it easier to figure out what kinds of types to use. It also means that in the autogenerated docs I can just click on the trait to find out what types I
can feed to the function -- in C++ many codebases have their own bespoke "trait" system using template specialization but it won't work with the docs and still lends itself to rabbit-hole-y error messages.
Moving: I personally find the lack of copy/move constructors and default construction to be very nice. In Rust, uninitialized types aren't a thing; initialization is always explicit. Copies are just copies, moves are also just copies, and moving is the default (except for POD types). There's no unknown overhead to deal with when I push to a vector, for example. Move-by-default and affine types IMO make it very easy to think about the program.