Why and Why not Rust?

What can I do with Rust?
Who is the competitor of Rust?
What are the good and bad sides?

Use Rust for reliable, high-performance systems programming. Use it if you want software to work in ten years and for safety guarantees to be ensured by the compiler.

If you want a simple language to quickly get apps deployed, instead consider languages like Go.

Rust is extremely powerful but has a higher initial barrier to entry than most languages due to its type system and ownership model (affine types).

1 Like
4 Likes

It's a general purpose programming language, so you can do pretty much everything with it. It isn't a scripting language though.

Difficult but interesting question. I think C++ is an alternative. Maybe to some extent also Java/Kotlin and C#, though these use garbage collection while Rust and C++ can do without.

Good sides:

  • memory safety by default (unless you use unsafe Rust)
  • speed (as fast as C, I think)
  • support for async
  • good package system and testing framework
  • memory effecient (regarding memory use, not program size!)
  • stability

Bad sides (my perspective):

  • difficult to master
  • sometimes prone to boilerplate code or verbose syntax due to some limitations of the type system (e.g. regarding method delegation)
  • async has been added to the language at a later stage, so async integration sometimes feels limited and complex (pinning, lifetime issues, etc.)
  • error handling sometimes feels like having a lot of friction (one thing I ran into can be found here, another example there)
  • complexity of the language leads to inevitable design flaws in std (e.g. this one) which are probably impossible to fix

Overall, I wished for a better abstraction of certain core concepts, especially in regard to control flow (like fallibility, yieldable closures).


P.S.: Regarding these negative sides, one should also consider that there's probably no real alternative around currently, which would fix all these issues (while keeping Rust's advantages).

2 Likes

A few remarks on your reply there:

Re: "Speed" - Rust code can certainly be as fast as C. Have a look at the "Benchmark game" site for examples. My early experiments in Rust were writing the equivalent of some little C programs I have in Rust and they easily matched C. Had they not I would not be here. Mind you using "idiomatic Rust" with iterators and such can slow things down. Using a more C style in Rust is often better, but not always...

Re: "Memory efficient" - I wonder what you mean by "not program size". People are using Rust on tiny micro-controllers Even down to the AVR's that have very little memory. Ciff Biffle has such an example, producing VGA graphics from a micro-controller that is smaller than his C version.

Re: "Difficult to master" - Mmm...compared to what? C++ is so complex nobody can understand all of it's features and how they interact with each other. The likes of Occaml and such are beyond me even after at a few attempts. Even Javascript has enough gotchas to have people scraping their heads for a long time.

Re: "error handling sometimes feels like having a lot of friction". Again, compared to what? I find Rust's error handling far nicer than the chaos of exceptions in other languages or the total lack of error handling in others.

Re: "complexity of the language leads to inevitable design flaws in std" Perhaps. On the other hand that is not unique to Rust. The "simplicity" of C lead to it having horrible API's in its standard library. For example all the "strxxx" routines.

3 Likes

I feel like many Rust programs have a large binary size, e.g. due to monomorphization. However, this is an advantage in execution time.

Also, typically Rust programs come with a lot of dependencies. Having hundreds of dependencies is not unusual. (Of course that's the same for many other languages with exhaustive ecosystems.) I think it's possible to program more "lightweight" though (and Cargo's features certainly help).

See also this follow-up.

You could compare Rust to languages which have lesser language constructs and which are better documented. Of course these other languages have other cons, but certainly, they are easier to learn. Just to name an example, completely understanding Lua is possible within 24 hours. For Rust, I would take years if I consider topics such as pinning, async, lifetimes, aliasing rules, threading, traits, closures, dyn vs impl, supertraits, recursive structures, error handling, channels, synchronization, drop handlers, stack unwinding, reference counting, subtyping, variance, etc. etc.

I found out that when APIs use callbacks but don't provide a fallible variant for them, then they will simply not support error handling at all (other than panicking). This made me wonder if it has been a good idea to not use exceptions for error handling. See also: Closures in API design – theoretical limitations and best practices?

std deals with this by providing multiple methods, e.g. Iterator::try_for_each in addition to Iterator::for_each. When writing code yourself, that gets you back to the issue of boilerplate, as I mentioned above.

So maybe languages which support exceptions are better in that matter (because they circumvent these problems)? I don't know. I felt like I spent a lot of time dealing with (and working around) issues regarding error handling in Rust – at least when I tried to do it in a well-structured way.

Yeah, C is certainly not better in that regard. I wonder though, if there are other complex languages which do a better job at fixing mistakes in their standard library.

On the pro-side, I would like to add that Rust does fix flaws in the language itself while maintaining backward compatibility by using Editions, which I really like.

What can I do with Rust?

AFAIK: anything your computer can do, van be done with Rust. I am rather new to the language, but not before checking some alternatives. I think Rust is especially good in NOT being optimised for a certain type of problems. That would be a general purpose language to me, and Rust fits the description.

Who is the competitor of Rust?

If you accept that Rust is NOT optimised for a certain type of problems, then any general purpose language would be a competitor to consider. Personally I compared to C (the language I think I know), Python (the language everybody knows these days) and C++ (the only realistic follow up to C from the time I learned C, that is the late 1980’s, early 1990’s)

I don’t like Python for being more or less an interpreter-language, but most of all because I don’t like the aesthetics of the source-code. Call me an idiot, but I want source to look nice, and the indentation-demands, the wording of the code, the entire way it works, I just wouldn’t frame it and put it on the wall in my living room.

So as I was looking for something new to learn after C, the only competition that remained was C++. I do believe anything Rust can do, C++ can do as well. But many people warned me the C++ from the early days isn’t the C++ of today. A nice thing about Rust is that it is still new, fresh, and coherently developed. C++, as I understand it, has become a bowl of spaghetti in its design.

Besides that, C++ is, like C, absurdly powerful, and allows you to destroy about anything. Anything compiles, the disasters happen at run-time. Rust on the other hand, as I understand it, is almost as powerful, but nothing compiles, so no disasters happen at run-time.

What are the good and bas sides?

Good:

  • it can make your computer do whatever your computer is capable of
  • it will not allow (!) you to crash your computer without you explicitly wanting to crash it
  • if you work, like I do, on a very simple linux-system with no IDE, only git & nano as your tools, you can just go on like that, but replacing the makefile-system you are used to from C by the cargo-system from Rust, well. That IS a good thing.

Bad:

  • debugging like you used to do in C is just gone. Is that bad? No, that is great. But it is replaced by a compiler with Cartman-features: respect my authority!!. No, it isn’t really bad, but it can be frustrating how the compiler will do anything but compile.

Ouch! Whilst I do love minimalism in that situation I would at least use vim for editing along with the rust-analyser. (actually I think neovim is required to use rust-analyser). The highlighting of syntax errors, type hints, and so as you type are very helpful.

See: Configuring Vim for Rust development - LogRocket Blog

3 Likes

Without an IDE, I would highly recommend running some watch tool next to your editor for fast and convenient compiler feedback. I personally love this one called bacon.

I use it even with (i. e. next to) my IDE, as I feel it gives a better overview, and also easier access to the full and proper error messages.

2 Likes

I find it interesting that nobody has brought up the competitors to Rust in the sense of "new systems-level programming language aiming to displace C and C++", of which there are several:

And lots more smaller ones. These are mostly more immature than Rust (which is, depending on how you count, about a decade old now, but they have interestingly different approaches which might fit what you're after better.

2 Likes

It's almost like a Cambrian explosion - though many of the languages are based on LLVM, and no alternative. Rust too - I think that is a weakness.

1 Like

llvm itself may have led to the explosion. If you want an open-source optimizer which supports many architectures and has a strong history handling C++, llvm and GCC are about your only options, and llvm has the easier license to deal with. Plus gcc's optimizer has historically not been available as a library (maybe that's changed?)

1 Like

I'm curious how these related to some of Rust's features.

What I found first very nice about Rust is how Rust distinguishes between passing arguments by value, by shared reference and by mutable reference. What I disliked about many other languages when they deal with references is that I can't see if passing an argument by reference is "destroying" or modifying the argument.

I later learned that Rust's mut isn't actually (always) about mutation but more about mutually exclusive access (i.e. no aliasing). Still, in many situations (given there is no interior mutability), looking at a function's signature shows you whether an argument is

  • consumed (x),
  • used in a read-only fashion (&x),
  • possibly modified (&mut x).

I think that's very helpful when looking at particular APIs or using them in your own code. Compare that to Python, for example, where calling foo(x) doesn't give you any information on whether foo will modify x.

How do Nim, Zig, Odin compare to this? Also, how do they compare to Rust in regard to not needing garbage collection?

Speaking of other alternatives, there is also Koka, which I recently got pointed to; though I don't really consider it an alternative yet because it doesn't seem ready for productive use yet. However, I find it interesting how both Rust and Koka deal with

  • avoiding implicit mutation when passing arguments (and in case of Koka also any side-effect [1]),
  • cleaning up garbage just in time (and not waiting for garbage collection).

This post and the linked master thesis helped me understand better how Koka tackles these issues (in comparison to Rust; while avoiding something like interior mutability).

I'm curious if those other evolving languages (Nim, Zig, Odin, …) also tackle these two issues, i.e. declaring mutation and avoiding garbage collection (which I would consider being two core features of Rust).


  1. including non-termination, unless declared ↩︎

Speaking as someone who doesn't use these languages, but I tend to hang out in their communities (and I'm ignoring any arguments that I perceive to be made in obvious bad faith): I get the sense that it boils down to an argument that Rust takes things too far; on a scale between C (too many foot guns) and Rust (too constrained), there's a happy middle-ground where you can get the best of both worlds. They don't mind that their languages still have all the foot guns out in the open, but the languages/libraries should require you to be very explicit about doing things that could trigger them [so that way you basically don't].

For my part, I've come to what I'm afraid is a pretty deeply cynical view of humanity: We're incredibly good at inventing stuff that we're not "good enough" to actually wield (relating to basically everything we do). In relation to programming; just look at how much time we spend writing new code vs fixing bugs in code we've previously written. My hot take is that Rust is a, probably not conscious, realization of this human design flaw; Rust acknowledges that no matter how hard we try, we're always going to trigger those foot guns if left in the open, so we're going to hide them in a special "safe" (which we ironically call unsafe) and stigmatize its use.

And that's basically my "Why Rust?" argument. Empirically speaking, we have enough data to show that we, as a species, can't handle unconstrained languages.

8 Likes

I personally don't think that this is a one-dimensional scale between "lax" and "strict" where Rust is on the "strict" side.

Some examples where Rust is more lax:

  • Rust doesn't track side-effects at compile-time (e.g. it's not a purely functional language and it doesn't have a type system that reflects effects),
  • Rust allows you to access data structures after a stack-unwind (AssertUnwindSafe),
  • Rust allows any function to allocate, leak memory, or panic, e.g. when an unwrap fails.

I can imagine far stricter languages:

  • not allowing side-effects unless declared (e.g. Haskell or Koka),
  • not allowing panicking or non-termination unless declared (e.g. Koka),
  • not allowing to continue a program with a half-unwound stack after a panic occurred (Rust can opt-in to aborting panics though).

I think Rust is perceived as being so "strict" because it requires that resources are released by default at the end of the scope they were declared in. The compiler ensures that nobody can access a resource after it has been released, but it's the programmer's task to help the compiler reasoning about it (e.g. using lifetime bounds). If you fail to provide the proper reasoning, the compiler will refuse to compile your program (for your own safety).

But is this about being strict versus lax? I'm not sure. I think this is more about trying to be efficient while staying safe (at compile-time).

I don't believe you need to make compromises between strictness and flexibility. Ideally, a language would provide memory safety as well as efficient resource release without demanding the programmer to aid the compiler. Such a language could still have a very strict type system.

Getting back to the question, "Why and Why not Rust?", I think a downside of using Rust is that you have to deal with aiding the compiler here and there by:

  • adding lifetime bounds and/or lifetime annotations,
  • deciding when and where to use reference counting (e.g. Arc),
  • properly using Pin wrappers where an API demands them and/or pin! values where needed.

I feel like ideally these tasks wouldn't be loaded onto the programmer; at least if it doesn't affect runtime efficiency. This is why I'm very much fascinated by Koka, as Koka does not require the programmer to deal with lifetimes. Instead, it uses reference counting everywhere, but attempts to get rid of that at compile time where possible (but at the compiler's discretion!). I don't know if these concepts are really competitive to Rust's approach in real life, though.

1 Like

I wonder what you mean there? How many dimensions can there be? I take the meaning of "strict" as "there are many rules", and "lax" as there are "few rules" (and they are enforced of course) Rules like:
Assigning to an immutable is not allowed.
Assigning a string to an int is not allowed
Calling a null or uninitialised pointer is not allowed.
Returning references to things that drop out of scope is not allowed.
And a thousand others.

Then number of rules is on the one demential number line.

Ideally yes. As far as I understand though, without lifetime annotations and such the compiler would have to completely analyse all of your code to see where it might now wrong. Which, if it is possible at all, would take a long time. I guess it is not possible because whether your program misbehaves or not depends on what inputs it has at run-time, which the compiler knows nothing about.

Hence we need to make compromises. Like using lifetime tick marks.

Note that the value of annotations for source code is less about being able to know if the code is valid as it currently exists (the compiler has to double check everything anyway) and more about knowing what to complain about when it is wrong, and being able to write code against only the assumptions you have of where other code will be in the future.

Otherwise you get the issue of some innocuous change, possibly deep in a library, causing code multiple layers away breaking. Rust actually does have at least one case of this behavior: async fns can lose their Send or Sync result bounds due to a simple change deep in their call stacks, and it's one of the worse Rust error messages (in the sense of being able to tell who did something wrong).

1 Like

I wanted to provide the opposite take on this, that sometimes I wish for a bunch of these annotation even in a GC language, because they're useful for communicating.

As an example of that, see my post in Str::as_bytes (again) - #3 by scottmcm

GC is great, but it doesn't actually solve the "you used that after you shouldn't have" problem, because it just extends lifetimes arbitrarily long and gives everyone mutable ownership of it. And most of the time, that's not actually what I wanted!

I want to write in the program that this is a request-scoped object, because without that I can't get a compiler error for "hey, you put that HTTP response header object in a shared cache and wrote to it from a different request". I want to write in the program "I need to look at this, but I promise I won't change it and don't need to hold onto it". I want to see in the signature "the lifetime of the output comes from the input, so it must be a substring and clearly isn't allocating".

When you don't have annotations like this, you end up with things like CA1819: Properties should not return arrays (code analysis) - .NET | Microsoft Learn because all you can do is share ownership.

When you don't have annotations like this, you end up with false-positive-heavy things like CA2000: Dispose objects before losing scope (code analysis) - .NET | Microsoft Learn because there's nothing in the function signature distinguishing who owns the thing, and whether this is a factory (that returns ownership) or not.

For any interesting resource -- sockets, locks, files, whatever -- I usually don't want the language to just silently let me use it out forever into the future.

6 Likes

Hmmm, I didn't think of that. So basically it can be seen as a guarantee when certain resources will (not) be released. Interesting.

So in a language where resource release is manually controlled (which is the case in Rust, I guess, even though it usually happens implicit at the end of a block) they are actually pretty helpful and not just a burden. :smiley:

Rules are not interchangeable, so is a language stricter when it has 4 rules less but 5 others more? I would say it depends on the rules and how you weigh them. (mathematically speaking)