3 ms·
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 saf
by Manishearth 10y ago
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<T>` -- `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<T: Frob>(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.
- keldaris 10y agoThank you, this is exactly the sort of comment I was hoping to inspire, and it gave me some food for thought. On first glance, I can see some utility in the ADT matching, but not necessarily something I miss in C++ (std::variant + templates gets me most of the way, although there are some issues with that approach). On the other hand, the clean separation of concerns and clear syntax of the traits example looks very useful indeed, and quite tempting.
- Manishearth 10y agoYeah, my point about std::variant is that it doesn't get used pervasively. With a few standardized macros for pattern matching we could get someplace, but right now, like with most new C++ features, codebases and libraries are very selective about the ones they use. This means that your natural C++ "style" can't rely on them, and not all libraries are written with the pattern in mind. OTOH in Rust everything is designed using enums, and any Rust code I write will be free to use enums. It works out quite nicely. In Rust it's not a tool you sometimes might reach for, it's a tool you reach for a lot, because most data is an "or" type.