When compared to more traditional languages like C/C++/Java, do you find that extra time you spend getting Rust code to compile, sorting out borrow check issues, etc. is mostly paid back via a reduced test/debug/fix cycle?
I absolutely prefer spending time getting things to compile on Rust, than debugging those things in C/C++/Java, even if it takes much longer. In other words, yes, the time is absolutely paid back(at least for the things I've worked on thus far).
Cause Rust provides guarantees wr.t. memory safety and concurrency related things, while others provide confidence at most, via static analyzers or dynamic analyzers.
No amount of testing or even fuzzing would equate to the guarnatees Rust provides.
(If you're using really heavy weight static analyzers or specification checkers that provide proofs of some sort then that's another story. Just talking about normal static analyzers that aim to find bugs rather than providing proof of security in this context.)
I'll challenge the premise, actually. I feel I spend less time in Rust writing the code the first place (including fixing borrow checker errors, etc) than I did in C++, since in C++ I needed to spend so much mental effort worrying about whether my references are ok, whether my pointers might be null, etc.
The comparison to C# (basically Java, but I'm more familiar with it) is less obvious; it depends greatly what you're doing. Much of the time one uses C# because they're fine spending some CPU and memory for more productivity, and that's great and appropriate. Seriously-perf-critical C# code, though, might not be easier then Rust -- manual arena management in C# to avoid GC costs is no fun, for example.
Most definitely! It's a lot nicer to get a type X doesn't implement Sync
error at compile time than trying to track down concurrency bugs because someone thought they'd use threads for a thing and didn't properly understand the consequences. I've had to debug poorly written multithreaded code in the past and it feels like there's a little gremlin inside your codebase causing things to break when you know it should be impossible.
Ownership and Option
also mean null pointer dereferencing or accidentally doing a double free are a thing of the past. At work I've had to try and debug a massive GUI application in a language where all memory management is done manually and this sort of stuff adds a massive amount of cognitive load and wasted time.
I'm going to agree with @scottmcm here. I feel like you spend less time developing in Rust than other languages because the type system catches most of the bugs for you. It's considerably easier to know a function returns an Option<T>
so I have to deal with the possibility I don't get anything, than to get a raw pointer back and remember whether I already know if it's already been checked/initialised.
Having a strong type system and a compiler with decent error messages massively reduces the cognitive load. Simply put, "if it compiles, it's probably correct".
I would also add that besides the language itself, there are other things that I find more productive in Rust. Namely things like cargo check
(and more specifically cargo watch
), along with very good built-in testing and documentation generating support.
Those things alone are enough to make going back to other languages painful for me.
I think a lot of this depends on the type of Rust code you write. For instance, working with tokio/futures, as they’re today, is not very pleasant until you get very familiar with those libs and more advanced Rust concepts.
Designing APIs is also tougher because you have one more axis to consider - ownership/borrows. In addition, if you go heavy with generics and abstraction, you can fall afoul of various limitations/idiosyncrasies of type inference, coherence, type recursion, generic bounds, type aliases, associated type projections and so on. This is an area where if you don’t know all of these things upfront, you can design yourself into a corner, so to speak. This isn’t so much of an issue for internal APIs since you can code your way out of it but if you’re making public ones, and you mess this up, I can see it being unpleasant.
But, if you get all this right, it should reduce debugging significantly.
I'm fine with spending more time on test/debug/fix because it ensures you don't get away writing poor code
To be clear, I think you mean most of the memory and thread unsafety bugs. That’s a big class of bugs, no doubt, but the rest of the bugs you need to prevent yourself .
A few examples that spring to mind:
- Option - definitely great to have instead of null. However, you’ll see plenty of code unwrap() or expect()’ing them. In a language like C# or Java, that’s essentially the default dereferencing mode - if it’s null, you get an exception. In Rust, you panic the thread. You’ll see Option used extensively with take() when working with certain libs (futures/tokio comes to mind) so a value will transition between Some and None, possibly several times. If you mess it up, you’ll panic - so you still incur cognitive load to track those state transitions in your head.
- RefCell - used frequently to work around borrowck. Same thing - if you get tangled up in control flow, or don’t scope things properly or don’t drop the access guard, you’ll panic. Need to track things in your head to some degree.
- Memory arenas were mentioned - these are a PITA in any language
. If you look at a crate like
slab
it’s easy to mess things up at runtime.
Having said that, I think Rust is great! Don’t take this as some sort of criticism. But, let’s also not paint a rainbow and unicorn necessarily .
@vitalyd I wouldn't say it's just memory safety and concurrency bugs that can be solved (or at least handled more easily) by using the type system.
Like most other strongly typed languages, you can ascribe meaning to particular types and use that to ensure assumptions/invariants are always valid. This helps with actual business logic, not just low level implementation details like memory safety.
But yes, it's definitely not all rainbows and unicorns. Luckily the work I usually do in Rust is fairly vanilla and not trying to do non-standard things with ownership (arenas and graphs) or traits (diesel).
Yes, definitely. And Rust has some interesting features to help in this space. My only point (perhaps obvious but I'll say it anyway ) is that these aspects don't come for free - the author needs to use those features wisely, correctly, and in good taste to materialize that additional safety. Some scenarios, like the ones I mentioned upthread, unfortunately require use of constructs or design that negate some of the strictness of the compiler.
The memory and concurrency safety, however, are "out of the box" - there's nothing you really need to do (modulo writing unsafe code, but that's its own thing). People reading this thread unfamiliar with Rust should have realistic expectations about what exactly the type system prevents by default - the rest you need to work at it .
** The reason I'm quibbling over this is because I've had some conversations with people that were a bit put off by some of the sentiment in the Rust community that "I no longer have bugs - type system gets 'em all!" Of course I don't think that's representative of the community at large, but I've definitely come across threads before (here and elsewhere) where I can see where someone might get that perception.
100x this. Stable tokio/futures is a real pain. I'm excited about the async!
/await!
progress that is being made, but sadly haven't tried it myself.
The only thing I have spent more time with (and still failed to complete) was getting explicit lifetime borrows in structs working. It made a pervasive mess of &'a
throughout my entire API, and ultimately ended up in self-referential struct territory. Where I determined it a failure and cut my losses. (I've since switched away from borrows entirely, and use Box
only where absolutely necessary.)
But After learning Rust the hard way, I have really come to appreciate that it is suitable for most non-trivial applications. There are still a few ergonomic niceties coming down the pipeline that will make it even better. The Try
trait comes to mind, which will allow the ?
operator to work with types other than Result
. All of these things really add up in terms of productivity.
I still find myself hitting road blocks frequently with borrowck. But having these kinds of errors caught early in development is such a freeing experience. The one thing I like to brag about with Rust development is that [if I can get it to compile] it has never crashed. Panics, yes, but I haven't ever had a need for tools like Valgrind to clean up my mess.
I can tell you that there used to be a genuine mental stress in my mind due to all of the above in C++, a stress I didn't realise existed until I started writing Rust. It is no understatement that I'm a happier person now because I switched to Rust. That alone should be worth something even if we ignore everything else.
I'm in python land at work and I can't tell you how much time is wasted because of the lack of basic type checking. It's soul destroying. You absolutely need to have great test coverage as otherwise the time just goes into a big black hole. Don't get me wrong, my daughter is learning python and I think it is a nice language in the small. But large scale python takes a whole lot of rigour. Holistically it would have been quicker to dev in Rust (or any other eagerly compiled language).
Never again!
That said at the moment Rust debuggers are poor relations to C#/Java. While Rust finds a lot more errors at compile time, it does currently take longer to hunt down runtime problems than C#/Java, but I'm sure the debuggers will improve swiftly over the next year or two to close down this gap.
(Yes I want it all, fast as C++, easy to debug as C#/Java and as safe as Rust, but I really do think we can get there.)
@gilescope what specific problems you experience with Rust debugging? I'm on Windows and using VS Code together with the Visual Studio debugger works brilliantly for me. Don't know about the non-Windows experience though.
I'm on OSX using lldb. I want to see &str and String as strings - one works, the other appears as an array.
I want to see hashtables in a sensible view rather than as a block of memory. I looked into the python visualisers for lldb but I don't think lldb has the type information to know where the key stops and the value begins.
There is a Rust Debugging Quest to slay some of these dragons (fingers crossed!):
Not a problem with Rust but with the programmer who abuses Option
. Not unwrap()
ping or expect()
ing is the point in having Option, so if some code does it regularly, that's simply bad-quality code, not a language design issue.
I'm a long time C and C++ programmer, and I am significantly more productive (more features per time unit) with Rust. I wouldn't even say I need to spend "extra time" getting the code compile. I basically came to Rust from modern C++, so most of the concepts around ownership, RAII, even the RWLock pattern, were familiar and natural.
However, the biggest win (and selling point) for me is fearless refactoring. Explicit error propagation (and the lack of classical "exceptions"), type-safety patterns such as newtypes, and real parametric polymorphism make it much faster to refactor my code and be sure it didn't change semantics.
I use this and I like it:
I agree in principle but some Rust libs (tokio/futures come to mind) make it necessary to use these things along with other less than stellar ones, like RefCell.