Discussion on programming reddit
C++ and D aren’t the only languages I know, I labeled myself that way in the title because as far as learning Rust is concerned, I figured they would be the most relevant in terms of the audience knowing where I’m coming from.
Since two years ago, my go-to task for learning a new programming language is to implement an MQTT broker in it. It was actually my 3rd project in D, but my first in Haskell and now that I have some time on my hands, it’s what I’m using to learn Rust. I started last week and have worked on it for about 3 days. As expected, writing an MQTT broker is a great source of insight into how a language really is. You know, the post-lovey-dovey phase. It’s like moving in together straight away instead of the first-date-like “here’s how you write a Scheme interpreter”.
I haven’t finished the project yet, I’m probably somewhere around the 75% mark, which isn’t too shabby for 3 days of work. Here are my impressions so far:
The good
The borrow checker. Not surprising since this is basically the whole point of the language. It’s interesting how much insight it gave me in how broken the code I’m writing elsewhere might be. This will be something I can use when I write in other systems languages, like how learning Haskell makes you wary of doing IO.
Cargo. Setting up, getting started, using someone’s code and unit testing what you write as you go along is painless and just works. Tests in parallel by default? Yes, please. I wonder where I’ve seen that before…
Traits. Is there another language other than D and Rust that make it this easy to use compile-time polymorphism? If there is, please let me know which one. Rust has an advantage here: as in Dylan (or so I read), the same trait can be used for runtime polymorphism.
Warnings. On by default, and I only had to install flycheck-rust in Emacs for syntax highlighting to just work. Good stuff.
Productivity. This was surprising, given the borrow checker’s infamy. It _does_ take a while to get my code to compile, but overall I’ve been able to get a good amound done with not that much time, given these are the first lines of Rust I’ve ever written.
Algebraic types and pattern matching. Even though I didn’t use the former.
Slices. Non-allocating views into data? Yes, please. Made the D programmer in me feel right at home.
Immutable by default. Need I say more?
Debugging. rust-gdb makes printing out values easy. I couldn’t figure out how to break on certain functions though, so I had to use the source file and line number instead.
No need to close a socket due to RAII. This was nice and even caught a bug for me. The reason being that I expected my socket to close because it was dropped, but my test failed. When I looked into it, the reference count was larger than 1 because I’d forgotten to remove the client’s subscriptions. The ref count was 0, the socket was dropped and closed, and the test passed. Nice.
No parens for match, if, for, …
The bad
The syntax. How many times can one write an ampersand in one’s source code? You’ll break new records. Speaking of which…
Explicit borrows. I really dislike the fact that I have to tell the compiler that I’m the function I’m calling is borrowing a parameter when the function signature itself only takes borrows. It won’t compile otherwise (which is good), but… since I can’t get it wrong what’s the point of having to express intent? In C++:
void fun(Widget& w); auto w = Widget(); fun(w); //NOT fun(&w) as in Rust
In Rust:
fn fun(w: &mut Widget); let w = Widget::new(); fun(&mut w); //fun(w) doesn't compile but I still need to spell out &mut. Sigh.
Display vs Debug. Printing out integers and strings with {} is fine, but try and do that with a Vec or HashMap and you have to use the weird {:?}. I kept getting the order of the two symbols wrong as well. It’s silly. Even the documentation for HashMap loops over each entry and prints them out individually. Ugh.
Having to rethink my code. More than once I had to find a different way to do the thing I wanted to do. 100% of the time it was because of the borrow checker. Maybe I couldn’t figure out the magical incantation that would get my code to compile, but in one case I went from “return a reference to an internal object, then call methods on it” to “find object and call method here right now”. Why? So I wouldn’t have to borrow it mutably twice. Because the compiler won’t let me. My code isn’t any safer and it was just annoying.
Rc<RefCell<T>> and Arc<Mutex<T>>. Besides the obvious “‘Nuff said”, why do I have to explicitly call .clone on Rc? It’s harder to use than std::shared_ptr.
Slices. Writing functions that slices and passing them vectors works well enough. I got tired of writing &var[..] though. Maybe I’m doing something wrong. Coming from D I wanted to avoid vectors and just slice arrays instead. Maybe that’s not Rusty. What about appending together some values to pass into a test? No Add impl for Vecs, so it’s massive pain. Sigh.
Statements vs Expressions. I haven’t yet made the mistake of forgetting/adding a semicolon, but I can see it happening.
No function overloading.
Serialization. There’s no way to do it well without reflection, and Rust is lacking here. I just did everything by hand, which was incredibly annoying. I’m spoiled though, in D I wrote what I think is a really good serialization library. Good in the lazy sense, I pretty much never have to write custom serialization code.
The ugly
Hashmaps. The language has operator overloading, but HashMap doesn’t use it. So it’s a very Java-like map.insert(key, value). If you want to create a HashMap with a literal… you can’t. There’s no equivalent macro to vec. You could write your own, but come on, this is a basic type from the standard library that will get used a lot. Even C++ does better!
Networking / concurrent IO. So I took a look at what my options were, and as far as my googling took me, it was to use native threads or a library called mio. mio’s API was… not the easiest to use so I punted and did what is the Rust standard library way of writing a server and used threads instead. I was sure I’d have performance problems down the road but it was something to worry about later. I went on writing my code, TDDed an implementation of a broker that wasn’t connected to the outside world and everything. At one point I realised that holding on to a mutable reference for subscribers wasn’t going to work so I used Rc<RefCell<Subscriber>> instead. It compiled, my tests passed, and all was good in the world. Then I tried actually using the broker from my threaded server. Since it’s not safe to use Rc<RefCell<>> in threads, this failed to compile. “Good!”, I thought, I changed Rc to Arc and RefCell to Mutex. Compile, run, …. deadlock. Oops. I had to learn mio after all. It wasn’t as bad as boost::asio but it wasn’t too far away either.
Comparing objects for identity. I just wanted to compare pointers. It was not fun. I had to write this:
fn is_same<T>(lhs: &T, rhs: &T) -> bool { lhs as *const T == rhs as *const T; } fn is_same_subscriber<T: Subscriber>(lhs: Rc<RefCell<T>>, rhs: Rc<RefCell<T>>) -> bool { is_same(&*lhs.borrow, &*rhs.borrow()); }
Yuck.
Summary
I thought I’d like Rust more than I actually do at this point. I’m glad I’m taking the time to learn it, but I’m not sure how likely I’ll choose to use it for any future project. Currently the only real advantage it has for me over D is that it has no runtime and could more easily be used on bare metal projects. But I’m unlikely to do any of those anytime soon.
I never thought I’d say this a few years back but…I like being able to fall back on a mark-and-sweep GC. I don’t have to use it in D either, so if it ever becomes a performance or latency problem I know how to deal with it. It seems to me to be far easier than getting the borrow checker to agree with me or having to change how I want to write my code.
We’ll see, I guess. Optimising the Rust implementation to be competitive with the D and Java ones is likely to be interesting.
Thanks for the article!
About “The bad” explicit borrowing:
Without explicit borrowing, passing a parameter could mean moving ownership instead of borrowing (in other languages it could mean copy instead of reference passing, but not in Rust, thanks to .clone()). With explicit borrowing, the caller can clearly see that:
– parameter passing is done through reference passing, which is cheaper than value passing
– the called method can or cannot modify the passed object (& or &mut)
In C++ and D, it could also mean moving. Rust is not unique in this. I understand the different decision, I just disagree with it.
I don’t know about D, but that isn’t true for C++.
Outside of a few obvious cases which produce rvalues, C++ requires you to use std::move to signify a move when passing a parameter. bbodi is right that C++ would call the copy constructor in the same syntactic case that rust would move.
In C++, I was thinking of calling a function with an rvalue. You’re right that if it’s an lvalue you have to wrap it with std::move. In D, rvalue references aren’t needed and doing this:
void func(Struct s);
auto s = Struct(…);
func(s);
Moves the s into func, just as Rust would. As mentioned in the reddit post, Rust isn’t unique here, it just chose a different path. I still disagree on the utility of said path.
Thanks for the post, it’s neat to get insight from an actual user instead of broad comparisons of features from a less practical perspective.
Still reading the article, but thanks for the MQTT broker idea! 😉 It seems real-word-ish and also not boring I believe.
Great article, thanks.
The attraction of Rust for me is the _total_ memory safety, as opposed to D and Go, which are kinda mostly memory-safe, most of time. Was this a factor for you? Did you find that Rust saved you from mistakes that might have bitten you in D?
So far, Rust caught exactly 1 bug, and that was due to reference counting, not the borrow checker. Every single time my code failed to compile because of borrows, I hadn’t written a bug. D is safe all the time as long as you call @safe code.
Well for memory safety risks there is user-after-free, memory leaks, and multiple threads accessing the same mutable object, right?
Go has garbage collection so user-after-free can’t happen (unless there’s a core language compiler or runtime bug) and memory leaks are less likely. The idiomatic way to do multi-threading is goroutines, which pass messages so there shouldn’t be multiple threads accessing the same mutable object.
D has garbage collection by default, so user-after-free can’t happen if you use default behavior (again, unless there’s a compiler or runtime bug) and memory leaks are less likely. D also stores one instance of a variable per thread by default, so you have to escape the default behavior to share mutable references across threads.
I think Rust is notable because it is as safe (safer?) than Go or D for memory without having garbage collection, and if you want safety and maximum speed that may make it the best option available. But I don’t think Go or D are significantly less safe – you just get higher memory use and much weaker latency guarantees.
Given my “your colleagues aren’t smart enough to use it” argument for tool choice, do you think you’d generally recommend Rust? Not asking about your current colleagues, of course, that would be rude. Asking about the idealized colleagues. 🙂
I’m not sure. It’s hard to get your code to compile at first, but at least they wouldn’t be able to commit really bad bugs. It’s easier to write C++, but it’s also a _lot_ easier to write bullet-in-foot C++.
[…] Rust impressions from a C++/D programmer, part 1 ::: Átila on Code […]
[…] up from my first post on Rust, I thought that after a week running a profiler and trying to optimise my code would have given me […]
I really enjoyed reading your article. I have some comments…
1) Rust is very explicit language, as you have noticed. It’s syntax of borrows in function calls allows you not to get into declaration every time you read someone elses code. I think this approach is really helpful during debuging.
2) Comparing objects for identity can be easily done by implementing core::cmp::PartialEq trait for custom structs.
3) Rust doesn’t have map! in standard library. It would be nice to see an appropriate RFC about this issue. Nonetheless there is an implementation of simple macro here: http://stackoverflow.com/a/27582993
Is there another language other than D and Rust that make it this easy to use compile-time polymorphism? If there is, please let me know which one.
You should check out pony it also traits and compile time polymorphism. You might fall in love again.
A colleague pointed out Pony the other day; I took one look and liked what I saw! Haven’t had time to do it in depth though.