I understand that the compiler helps catch bugs, and undefined behaviour, and I can't help but asking myself: how frequent are those in Python, or Typescript?
What I do "suspect" is that mutating Objects/Dicts and Arrays/Lists can be overlooked by a type system, and Rust would save those errors, IIUC.
Or maybe, in this case, the key benefit of Rust is speed due to less or no garbage collection, but I am unsure.
It's not about adding one or two snippets for Python or TS, I wonder whether there is an actual practical benefit in real world apps? Or general errors that one avoids? (also not comparing w/ typeless Python, or with JS, nor with people using Any/any.)
Maybe there is some blogpost you can recommend.
I loved Rust so far (I'm only ~ a week in though), but still have this doubt.
Very, very frequent. If you're a Python developer, have you ever hit this issue? Being upvoted 3490 times in StackOverflow (as of writing this), I would say that many, many developers are bitten by it.
And that's only about mutating arguments that are passed by reference. Rust type system is much, much richer than that; this is just one keyword (mut) in Rust's glossary so far, so to say.
There's a whole different family of bugs that are catched by the Send and Sync traits, for example. They are not so common to be hit in Python since most of the code is single-threaded, but that's also why it's more difficult to have sound and performant multi-threaded runtimes (i.e. Uvicorn vs. Gunicorn).
How much value would you get from using Rust vs. Python or TypeScript would totally depend on the nature and complexity of your application.
Thanks! I'll get to compare better myself as I go along. I've not ran into that issue, but certainly would have, thanks a lot for the article !
PS: btw, I think "bitten" does not exist? it must be "beaten" which is homophonous. I only say it because it's in the article as well where it also caught my attention. But I could be simply wrong.
Yes, the biggest advantage over python and typescript is that rust can be much faster and efficient thanks to being compiled and without a garbage collector.
If you don't need the additional performance then Rust still has a stronger type system that helps you reason with aliasing, but arguably for some this might not be worth the additional complexity.
Loving a programming language is a bit strange in my opinion. My feeling is, that for many task languages with a Garbage Collector are just good enough, e.g. Go, Python. People often tell that Go is much easier than Rust. And a language with a GC is memory safe, concurrency might be a problem. But Rust is not the solution for every thing, and in my opinion learning Rust for someone who never programmed before, just as a hobby, is a big task. I would no try that, at least not when I am already retired.
When I was first introduced to programming in tech school it was with BASIC. But soon we were expected to be somewhat proficient in assembler. That's on top of all the statistics and numerical analysis were expected to pick up in that CS course. Given that experience I don't see why Rust would be a problem.
Like a toddler learning English a new programmer does not have to learn all of Rust at once. A lot can be achieved without the "hard stuff" to start off with.
It would take some suitable teaching material aimed at the programmer novice though.
Nah what? I'm well past retirement age. That has not dampened my curiosity for new tech. Especially when it is the first compiled to native programming language since C to offer genuinely new and useful features. It's been a lot of fun.
On the face of it that is a bit strange. I mean computing is so logical and rational how could a different look or style of a language make any difference? As long as it allows the job to be done.
Turns out programmers are not so cold and rational.
Musicians can be very attached to their instrument, the way they feel as much as the way they sound never mind the style of music they play.
Carpenters can get very attached to their tools. Having practiced with them and customised them exactly to their liking over a long time.
So I come to see that programmers do indeed love their favourite languages. And editors, and operating systems. Like musicians and carpenters they have invested a lot of time and effort into honing their skills with these things. It's a very emotional attachment.
Well, on top of what has already been said, at least for typescript which Im more familiar with, Im not the biggest fan Duck Types, although they can be convenient.
Also, Rust has much better ergononics for handling types, things like pattern matching and helper methods from the standard library go a long way. Even though Rust is stricter than TS, it feels a lot less inconvenient, but thats just my subjective experience.
Also, speaking of standard library, at least for typescript/javascript thats a BIG difference, ecma moves slow af, things like Date, Map, Set are so barebones and inconsistent, but I suspect Python is a lot better in that reggard.
To show another benefit of a type system with explicit ownership; I was once writing Snake in Python, and even back then I knew I needed to know: if I pass a list to this function and mutate it, will the caller see the mutations?
So I ran a test, including all the details I thought could influence it. The answer was yes, and I relied on that answer in my code.
Three(ish) hours later, I'm tracking down a strange bug. Turns out, some strange, tiny detail must've made my list get passed by value instead of by reference.
Things like this are what made me move away from Python, despite it being the only language I knew: it tries to hide complexity, but it's still there. Languages like C++, Zig, and yes Rust, help the programmer out by letting them see the complexity, and manage it.
If you want to compare programming languages objectively, there are many factors:
Raw CPU performance -- maximizing this is often critical for systems/infrastructure software and computation-heavy algorithms, but not for everything.
Memory safety -- some languages (C/C++/Zig/etc) are not memory safe, which means certain bugs can cause security vulnerabilities and crashes.
Runtime overhead of garbage collection, which can impact the latency of server apps and increase memory usage, but can also make programming simpler.
A strong type system can reduce bugs, including logic errors.
Compatibility with other tools and libraries you need to use. This may be the biggest one, since to create something useful you need more than a language. I suggest deciding what you want to create first, and then selecting a language.
"Issues" like that are more to do with the perils of "side effects" than the type system. Rust also has similar "gotchas" (e.g., Copy types with mutable methods). Once you start mutating variables you leave the comfy confines of algebraic reasoning which is why languages like Haskell are greatly enjoyed by some.
Personally I don't find it strange. It's like linguists who "love" languages in general. As someone who quite enjoys languages across the entire spectrum from ultra rigorous languages like ZFC to less rigorous but still formal languages like Rust to ultra flexible languages like natural languages (e.g., American English), I still have my own preference and "love" for languages further on the "rigorous" part of the spectrum since I take great enjoyment in writing in them. There is something incredibly comforting about "proving" something correct. Languages like Rust are of course a far cry away from a "proof", but expressions in Rust are closer than expressions in some other programming languages. Once you've been bitten enough times by one's intuition or insistence that something is "right" without actually proving it, you tend to have increased skepticism and thus desire to ensure your program is in fact correct. It's all relative though. Keep going along on the spectrum and you start writing in Idris 2 and writing formal verification proofs in Coq which to some is "too much". To some, Rust is also "too much". To some, Python is "too much". We all have a line.
I'm more of a "mathematician" than an "engineer" though, and I certainly understand why one would not feel the same nor would I argue they are wrong for not feeling like I do. To each their own.
But, just adding bc I'm learning not to correct it (also i may be wrong ), mutating elements is still allowed in a similar case, right?
fn main() {
let mut v: Vec<i32> = vec![1, 2, 3];
for i in &mut v {
if *i==1{
*i=100;
}
}
println!("{v:?}"); // 100,2,3
}
Or similar, but this without a python equivalent maybe:
fn main() {
let mut v: Vec<i32> = vec![1, 2, 3];
let b = &mut v;
let c = &mut b[2];
*c=100;
println!("{b:?}");
println!("{v:?}");
}
I had forgotten that the mut borrow of v does not affect mut borrow of its elements (read about it a few days ago.) Seems slightly hard to remember for me.
Would you mind defining what a "benefit" is, for starters?
Not asking this in a tongue-and-cheek way either, to be clear: different people have different goals with different points of view, objectives, priorities - you name it. Some only care about the end result. Some only care about the speed in which they get to iterate on the release of their end result. Some only care about the performance (and their AWS/GCP/Azure bill). Some only care about their karma on Reddit and where they get to experience any dopamine rush after announcing yet another RIIR project to a bunch of strangers they otherwise wouldn't care about.
What do you care about, personally?
UB is specific to lower level programming. Higher level PLs impose strict limits on what is and isn't "valid code", all of which are checked at runtime. Low level PLs - such as C and C++ and Rust - let you write a bunch of numbers into memory, mark that memory as executable, and call into it as if it was a regular function. With that kind of flexibility (and "power") comes a whole new set of risks (and "responsibility") - some of which no compiler can account for. That's what UB is.
Bugs, on the other hand, are unavoidable - no matter the language you're using. Most of them will have to do with your program's logic: if you want to sum all the numbers from a user and instead you end up either substracting them, by typing - instead of +, that's entirely on you.
Some of them will have very little to do with your own lapses in judgement and/or short term memory access capability, however; and much more, with the given PL's choice about what's meant to be considered a peculiar "feature" and/or an "intuitive" part you just have to deal with.
Where things get most tricky and interesting and philosophical and "how dare you say <A> about <B> you feeble-minded <C>?" is in the built-in layers of abstraction (and/or hand-holding) that different people have (vastly) different levels of tolerance (and/or enthusiasm) for.
For instance:
should a type T implicitly coerce into a type S whenever the expression X expects it?
should the standard library come with its own garbage collector built-in?
should a function's default arguments be its own "member data"?
If you only ever happen to care about the end result (i.e. making your machine "do stuff"), you will probably welcome and embrace and rejoice at every attempt of your favourite PL's team to make your personal DXharder better faster stronger. It's the "vibe" that matters, after all.
If you only care about your speed of iteration towards the next MVP and/or round of funding, you will likely care exceedingly little about "getting it right" the first time around and a great deal more about the overall ecosystem's maturity and frameworks and packages and wrappers around the boring tedious hypophobic parts of your stack. [XKCD] comes to mind here.
If you only care about r̴̢̡̭͈̖̫̗̝̟͖͊͂͛͑̾̋͆́̒̀͗̾̾͘ą̸̝͕͚̜̞͔̭̼̝̪̖͕̲̳̠̼̋̓̋̂̈́̀̾̆ẅ̷̛̤̞̪͍̭̹̗̰́̂͒̈́ ̷͍͚̥̮̠̙͓̩̣͍̞͎͒ͅp̷̢̤̠̘̬̙̥͔͉̩̯͖͋̃̾̒̀̈́̉̓͌̐̈́̐́̏̅͜e̸̢̨̗̜̼̞̱̜̥͑ŗ̵̜̯͌̍̈́̉̓̅͛͂͂͛̋̽͐́f̸̡͈͈͚̳̼̜̳͎̿͒͋̃͛͋̾̅̉͜͠͝o̸͕̜̱̟͕̦̲̫̻̻̠͊̏̓̽̑͗͋͑̾͂̋̑̋̃̎͘r̷̛̳̰͙͇̳͙̙̝̫̤̝̭̝̃̔́̅̀̌͠m̸̥̮͚̼͙͇͎̯̋͂̋̑̃̅̓̃̇͒̕͝ą̵̢̥̣̲̇̒͗n̶͖̬̥͇̻͚̖͚̦͎͙̣̗̳̪̑̉͌̈́̃̓͠c̵̩̞͖̳̊́̀͂̓̐̾̍͜è̴̢̧̛͖͎̱̝̰̝͂̋̈́͑͐͝͝, you will keep calling everyone who uses anything either than pure unadulterated C (with maybe some asm thrown in, for good measure) many cute and interesting names that the rest will either ignore or promptly (if not passionately) reciprocate.
Rust learned quite a few good lessons from quite a few different languages, all the while keeping things as low-level as you might need them to be. You won't find any billion dollar mistake's built in, just to maximize backwards compatibility or to make things "more intuitive" for newcomers from other languages. Just because an idea is popular, doesn't mean it's great - to begin with.
Will any of it be of "benefit" to you? Without knowing a thing about you or your priorities, there's no way to tell. Rust is a tool. Hammer is a tool. Will a hammer be of any benefit to you? Depends on how and when and what you choose to hit with it. Same goes for Rust, or any other PL.
Frankly, learning any programming language for some one have never programmed before is a big task, but I'm still not convince that Rust is especially hard. That quote is very apt:
This is basically the programming version of "learning Japanese as an English speaker is hard, therefore it is not a good language for babies to learn"
We know for the fact that Rust tutorials are not designed for someone who have never programmed before… but we still have no idea if learning Rust as your first programming language, from scratch, is harder or easier than learning, e.g., JavaScript or Python.
Because many things that you may routinely do in JavaScript or Python are either hard to impossible in Rust – and you have to unlearn them – but they are also impossible in read world when you work with real world objects – thus someone without programming experience wouldn't need to unlearn them.
But one thing that we all may agree with is simply the fact that none of existing tutorials target that audience of someone who have never ever programmed before.
I suspect that this much harder thing for a novice compared to other issues with Rust. Take that chapter 4. You may teach it with simple real-world examples of how we handle nested tasks (if we decided to create a package to send someone a gift and then find out that we need a nice picture to send with it then we would pack everything, put it aside and pull crayons or brushes to do another task), then go with the need to have some objects that don't belong to particular task and other things based on interaction with real-world, physical objects (that maps to how Rust does ownership and borrow, even with the exact same words) this chapter dumps “execution stack” and “heap” on you… extremely advanced concepts that one don't even need to know at all to program in Rust!
Compiler does that so you don't have too. That's very nice property: you don't need to keep in mind all these rules, you can trust the compiler.
Of course sometimes compiler rejects the code that's perfectly valie (it have to, because it's simply just not possible to say with 100% correctness whether code is valid or not) and then you need to use unsafe and… that's not fun, yeah… it's dangerous and complicated part of the language. But Rust without unsafe is much safer than many other languages. And I suspect that may actually make it good as “first ever language”. Still needs an appropriate tutorial.
Yes, you're iterating over the list, but you have explicitly told the compiler that you want to iterate through a mutable reference.
A type system can only do as much. If the programmer still explicitly chooses to shoot themselves in the foot, it's no longer the compiler/type system's fault.
To be clear, Rust does not prevent this problem in general. You are more than able to implement Copy for any type that can implement it; thus causing your loop to compile for such types. More generally, mutating variables can cause confusion. For example:
#[derive(Clone, Copy)]
struct Seq(usize);
#[derive(Clone, Copy)]
struct SeqIter(usize);
impl Iterator for SeqIter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
let cur = Some(self.0);
self.0 = self.0.wrapping_add(1);
cur
}
}
impl Seq {
fn new() -> Self {
Self(0)
}
fn iter(&self) -> SeqIter {
SeqIter(self.0)
}
fn increment(&mut self) {
self.0 = self.0.wrapping_add(1);
}
}
fn main() {
let mut x = 0;
let y: Option<()> = None;
let _ = y.or_else(move || {
x += 1;
None
});
// Suppress any compiler warning about `x` not needing to be `mut`.
x += 1;
// When `Copy` types are `move`d, a _copy_ of them is `move`d which means mutations to `x` are actually
// mutations to a completely separate variable that just so happens to have the same name; thus
// `x` here is the original `x` and its value was _not_ mutated in the closure; otherwise it would be `2`.
assert_eq!(x, 1);
let mut seq = Seq::new();
let mut iter = seq.iter();
for n in iter {
// Why can I mutate `seq` if `Seq::iter` borrows `self`?!?
seq.increment();
// Why am I able to mutate `iter` while also iterating `iter`?!?
iter.next();
iter.next();
iter.next();
}
}
Two big differences that garbage-collected languages typically have compared to Rust is that because heap-allocated objects are maintained by an "omniscient" entity (i.e., the garbage collector), the language can trivially allow variables of such objects to be copied without fear of dangling pointers; furthermore such languages will often go further and not only allow such a thing but force that all variables are copy-able since many such languages don't care about data races which is still a problem when one copies two mutable "references".
While Rust (obviously) prevents any heap-allocated types from being Copy and further types that can be Copy have to still opt into it, a developer can still define an Iterator that implements Copy causing logic errors to still happen like the above loop.