This is in tune with my own experience using Rust in production: it can stop you from doing certain classes of mistakes, but it won't stop you from doing stupid things.
But the idea that I don't have to think about certain classes of problems allows me to give these stupid things more focus, which is surprisingly refreshing.
The predictable nature of Rust was so refreshing for me that I ended up using it even for smaller reusable scripts where I would happily have used Python before but soon got annoyed with obvious errors that would only show up once you run a program.
If you e.g. have a `print foo` in some obscure branch that rarely happens, that print will ruin your day if you use Python 3. If python would be a little like Rust you would get on save (or at least on compile) a hint or error, that the print should look like this: `print(foo)` for Python 3. You can be incredibly careful and rust will still catch things now and then, that would have gone unnoticed into production unless you have immense test coverage.
I like Rust for the experience I had with it. It definitly changed how I approach certain problems in a very good and productive way, even when I don't use it.
Agree completely. For a bit of my own story, a year+ ago I had the option to write a project in Rust and evaluated it vs Go. Long story short, I tried rust, and it was a massive headache and I failed. We used Go (as I had been for ~4 years).
Fast forward to ~2 months ago, a work project dictated tight control over memory which, while possible in Go, had me looking at alternatives. I decided to give Rust another try. This time it wasn't just an evaluation, it was needed to work so I bought and Rust book and spent some after hours time learning/etc.
This time, Rust has been an absolute joy. I have no understanding why last time was so painful, and this time it's been so amazing. Maybe it was the book[1]? Maybe it was just a 2nd round of learning based on my previous experience? Regardless, it's been great.
There's just so many mental overheads like what is concurrent safe, what is non-null, etc that are just great to not think about anymore. On top of that, the formatter and LSP are just great. It highlights in my text editor (Kakoune) what variable caused an error, where it gets moved incorrectly, etc. So much just works, it's great.
My only complaint these days is:
1. I find it odd that some things like slice reads can still panic by default. Yes, I can use `foo.get(1)` to avoid panics, but still - it's a bit odd to me.
2. I'm anxiously awaiting async/await. It's quite difficult to be patient.
[1]: Programming Rust: Fast, Safe Systems Development
> I find it odd that some things like slice reads can still panic by default. Yes, I can use `foo.get(1)` to avoid panics, but still - it's a bit odd to me.
Panicking is a perfectly good way to handle the situation where an invariant of the program is violated. That is, it is perfectly fine to index using [] in cases where there cannot possibly be an out-of-bound access unless the program has a logic bug. And if it does have a bug, there's usually not much that you can do except abort because you can't trust the program anymore.
On the other hand, if you know that your input might be OOB without violating a precondition, as a part of the normal execution of the program, and you can handle it gracefully, use `get` instead.
This distinction between "expected" and "unexpected" errors is an extremely valuable one, and one unnecessarily muddled by languages where all errors are signaled with exceptions. Rust gets it right.
I don't understand, tbh. Eg does Rust not make great effort to prevent null pointer exceptions? OOB feels quite similar to null pointer.
It's strange to me because a lot of what I value in Rust is writing code that handles all outcomes safely and with confidence. Yet that all seems to be out the window if you access array values - as it may or may not work.
The safety of [] slice/array access is entirely up to how the dev wrote the code.. which largely feels the same as any other language.
The usage pattern is somewhat different. The primary problem with null references in languages such as Java is that they are not opt-in, and they're not even opt-out! That is to say, you don't even have an option (heh!) to encode in the type system the fact that a reference is never null. What's worse, references tend to get passed around a lot, and stored inside objects, and programmers are lazy and don't assert or even think about preconditions, so the root cause of an unexpected NPE can be far removed from the code that finally triggers it.
With code that indexes slices, the index computation is usually much more proximate to the actual indexing operation. If ensuring the validity of the index is about upholding an invariant internal to the module, it makes no sense to return an "sorry, I have a bug" error to the caller if that invariant fails to hold. What would the caller do about that?
I think the slightly odd or inconsistent thing is that rust has [] at all. I think of it as a compromise for people's familiarity with the operator. And if that's all it is, it makes sense that it behaves the way it does; I think a panic is more similar to what other languages with that operator do with an out of bound index than returning Option would be.
It’s probably a combination of both things, but “I tried rust, struggled, quit, came back months or years later and now I have no idea why I thought it was so hard” is certainly a recurring pattern we’ve seen. Glad it’s working for you now!
Both type inference and borrow checking have also received a whole bunch of small improvements over the past couple years. Subtle enough that you probably wouldn't notice them except in that the language just takes less effort to use.
Also error messages have had a lot of effort put into them, so an error that would have been confusing a few years ago, now may no longer be confusing.
For real Beginners to programming Rust can be hard, because concepts like pointers are hard in most languages that use them.
For Programmers that try Rust out Rust can be hard because they are stubornly trying to program Rust as if it were Python/Java/C/C++/Foo – but it isn't.
Rust as a language makes certain types of approaches (or anti patterns) nearly impossible. In the beginning it happens quite often that after a day of struggle you will erase a Rube-Goldberg-Machine that does something simple and replace it with the right line of code that does exactly the same while beeing way more extensible.
If you find yourself building more and more elaborate structures just to get something very simple done, the problem is very likely your approach (e.g. trying to implement a OOP structure instead of solving the problem) – at least that is what happend to me a lot.
Rust's borrowing and ownership concepts, as well as traits and generics make it extremely powerful, but these concepts lend themselves better to certain ways of structuring code than to others. Learning something is in my opinion always about allowing something to change your perspective. If you are not allowing the thing you are learning to change you, chances are that you are not learning, but judging.
From my friends that have tried rust, creating a graph or doubly linked list without using some special structures is fairly painful or impossible. You can create a graph by using some sort of adjacency matrix or some other kind of structure that keeps ownership in some sort of tree without much pain, but that has it's own downsides.
Have not used rust but I'm assuming it's because when you're down a rat hole rust stops you cold at every turn. You need to back up and rethink what you're trying to do. But when you don't have a good handle on what rust is complaining about you can't see that.
The books are incredibly well written. When people are “stopped cold at every turn” they are usually programmers who come to Rust with a metric ton of existing assumptions how to solve certain problems, without really thinking about the concepts of the language itself.
When a C++ programmer starts with python they will write incedibly “unpythonic” code, when they do the same thing in Rust, it just won’t compile.
IMO Rust is very straightforward and the std library is incredibly good. But you have to really understand certain core concepts and what they mean in terms of structuring your code.
Just like that C++ programmer needed to learn what is ideomatic python looks like and why it makes a lot more sense to write python that way, you need to find the rust way of things. But this is the same for every progtamming language out there..
There's plenty of languages which take limited / little efforts to learn. So I'd say more that the unfamiliar concepts need to stew or sink in.
One way is to just keep at it until it clicks. An other way is to not do that, but once you've got the words for something you start seeing the issue everywhere, and next time around it makes a lot more sense.
> I have no understanding why last time was so painful, and this time it's been so amazing.
It has been my experience that if I try to learn something hard and give up and then come back to it a few months later, I have a way easier time even with no apparent changes in process. I suspect some of it is giving your brain time to process fundamentals or something.
> 1. I find it odd that some things like slice reads can still panic by default. Yes, I can use `foo.get(1)` to avoid panics, but still - it's a bit odd to me.
I wonder if this is similar to C++'s `[]` vs `at`. `at` does implicit bounds checking but, as an optimization, if you are already doing an explicit bounds check, you can elide the implicit check via `[]`.
It's more about ergonomics. With `get` you get an `Option` which allows you to handle the out-of-bounds situation, but is unnecessarily noisy when you know that the index must be valid unless the program is buggy (and in that case you most likely want to abort anyway).
It's somewhat similar, except C++'s [] leads to memory unsafety whereas Rust's [] on slices has bounds checks and will abort the program on failure. Your program will crash but it won't be an exploitable bug (except for a DOS). .get(), meanwhile, returns an Option so that code written using it can recover from OOB accesses.
C++ at() doesn't check and abort, it checks and throws a specific documented exception that you can catch. So it is, in fact, closer to get(), just using a very inefficient way of reporting.
> C++ at() doesn't check and abort, it checks and throws a specific documented exception that you can catch. So it is, in fact, closer to get(), just using a very inefficient way of reporting.
You can catch a panic, and you can compile C++ with -fno-exception. at() is not closer to get() than to [].
I wonder if it has something to do with the nature of the problems. Rust can be very smooth or a real pain, depending on the data structures you need to model - basically, whenever ownership is unclear and sharing is common, it's going to be more painful.
Usually that just means you have to think much harder about the problem you are trying to solve within the new set of requirements (borrow checker, lifetimes).
Weirdly enough these things should also be a hard problem in other non-Garbagecollected languages, but you get away with a lot more when the compiler doesn’t force you.
From a purely practical level there is so much already that I had a hard time finding some data type that had no existing and tested solution for it. Looking at the code in the std library can be enlightening.
> Weirdly enough these things should also be a hard problem in other non-Garbagecollected languages, but you get away with a lot more when the compiler doesn’t force you.
It's the same as with static vs dynamic typing - some things are just easier to model with dynamic, because you don't have to fight with the type checker to prove that your program really is sound when you know that it is. But, of course, you might believe it to be sound when it really isn't...
Love it - was a Vim user for many years before that. I will say that while the Unix-y design is really cool, the plugin system written around Unix (Bash) is a bit of a pain. You technically don't need to use bash at all, but idiomatic plugins tend too.. so.. meh.
These days I'm wanting out of the Terminal - but wherever I go, I'll need to take Kakoune's style of modal navigation with me. I love it too much.
_(For reference, I'm hoping Xi editor implements the foundation of modal editing, and then I can make Kakoune style editing work in Xi Editor)_
> before but soon got annoyed with obvious errors that would only show up once you run a program.
From what I gather you are comparing static vs dynamic typing, and I agree. I do not use Python because I prefer a subset of errors caught at compile-time. However, it is silly to make it sound like that this is somehow limited to Rust. You could just as well have used Go or OCaml and feel "refreshed" because obvious errors would have been caught at compile-time.
> The predictable nature of Rust
Again, given the context, it is static vs dynamic typing. This is not something limited to Rust. In case you were not talking about static typing and its pros, could you please tell me what kind of official formal proof tools exist for Rust that makes it predictable?
Sorry if I angered you. I never claimed that Rust was the only language that has these properties.
My experience was is: I have never seen a language/environment where these rules were so well thought out, so well communicated and so well enforced. The package managment works like a charm, the module system (since edition 2018) is the best I have seen etc.
These are all subjective – I am a film/philosophy student who codes and I never received any formal education on any CS related topic. I was speaking about why Rust feels sound to me. Good chances Haskell will look sound to me too, very good chance I might check out more CS topics. No hard feelings please.
It is fine, and no it didn't anger me, so no need to apologize, really. :)
> I never claimed that Rust was the only language that has these properties.
Yeah, that is my mistake. Sorry for misunderstanding you!
> I never received any formal education on any CS related topic
Same here actually! :P
> No hard feelings please.
Not at all!
I understand that you like coding in Rust, and that is completely okay with me, and even if it was not, that should not deter you from continuing what you are doing.
>could you please tell me what kind of official formal proof tools exist for Rust that makes it predictable?
I feel like you aren't really asking a question, that your intent was really to lecture someone, but in case you really are:
* a borrow checker
* option types
So OCaml would catch a similar set of obvious errors, though Rust would also catch all data races at compile time, as the borrow checker does that as well as prevent memory mistakes.
No, I was really curious. My bad if it looked like I was trying to lecture. I do not think that I have the necessary knowledge in this topic to do that. :)
> a borrow checker
What exactly do you mean? How does it differ from any other language's type system?
Which mistakes are you referring to specifically? I need to know so I can have a meaningful response to it, but all I can say right now is that Ada/SPARK does the same.
> What exactly do you mean? How does it differ from any other language's type system?
Part of rust's type system is sub-structural: by default, Rust's types are affine, which means you can only use them once (at most, not exactly).
Now this is not super convenient to use and could have efficiency issues (e.g. any time you want to check a value in a structure you'd have to return the structure so the caller can get it back), so to complement this you can borrow (create a reference). The borrow checker is the bit which checks that borrows satisfy a bunch of safety rules, mostly that a borrow can't outlive its source, and that you can have a single mutable borrow or any number of immutable borrows concurrently.
The borrow checker provides for memory-safe pointers to or into a structure you're not the owner of, with no runtime cost.
> What exactly do you mean? How does it differ from any other language's type system?
If you're familiar with affine types, this is basically what Rust's borrow checker implements.
If you're not familiar, it's a bit complicated to summarize on Hacker News. You should try it out, because it lets you guarantee statically entire classes of properties that non-academic languages struggle with.
Can you recommend a good book on SPARK in general? I've read one on Ada 2012 itself, and what it had to say about Ravenscar and SPARK got me interested.
There is also "High Integrity Software: The SPARK Approach to Safety and Security", I never read it, but it is written by a well known author in the Ada community.
That’s one aspect of the errors that are avoided, but there are many others too - some of which aren’t solved by other languages. Go, for example, will happily nail through all of your obviously null pointers with gay abandon. The Rust ownership model prevents other classes of bugs.
It’s not a panacea—and to be blunt, I find Rust more work than it is worth for the sort of projects I typically work on. But any tools which can eliminate whole error categories are worth looking at for sure!
I was responding to the issue I quoted, which really is just a matter of using a programming language with static typing. :)
> some of which aren’t solved by other languages
You only mentioned null pointers, so I will go with that. In Ada, you can have access types (pointers) that are guaranteed to not be null, and accessibility rules of Ada prevent dangling references to declared objects or data that no longer exists, so this particular issue is solved by a language other than Rust. Please feel free to give me other examples of errors or issues that you may believe is not solved by languages other than Rust.
I will give you a few examples. My knowledge of Ada is insufficient, so I'll let you tell me whether Ada can solve that without using an external prover (note: being able to use an external prover is great and I haven't seen this done in Rust yet :) – but that's not the topic at hand).
1/ Consider a file `f` (or a socket, etc.). Using the standard library, Rust will statically ensure that, once the file is closed, you cannot attempt to, say, read from it. This is nothing special to files, just an aspect of the borrow checker.
2/ Consider a communication protocol. You need to send a message `HLO`, expect a message `ACK`, then send something else, etc. It is pretty easy to design your objects such that the operations of sending the message, receiving the message, etc. will change the type of your protocol object, ensuring statically that you never send/expect a message that you're not supposed to send in the current state.
Ada is awesome, and if compiler writers had been more timely it might have taken hold. Rust solves the same problems without a runtime, and about 3 times the performance. Granted it looks like c++ and ocaml made a particularly ugly child.
>> [...] solves the same problems without a runtime, and about 3 times the performance.
I think you should look more into Ada. The only "runtime" is for the exception handling and bounds checking, both of which can be turned off if needed.
And I don't know where you got that "3 times" figure from? Do you have an example you are referring to?
You can do everything that has been mentioned without Ada's RTS, you can also disable all run-time checks. You can use static analysis tools (there are many, and available for free), for example, you can formally verify the correctness of the program, no run-time checks required.
The hard thing is not understanding the concepts but learning what this implies for your coding style, especially when you come from a garbage collected language like Python.
Both concept can at times give you hard to solve tasks, but it usually is because the problem you are trying to solve is hard and you cannot just naively hack away without thinking long and hard about it.
Other than that I learned liking the borrow checker and lifetimes because you will get a incredible narrow scope of which variables can matter when — which makes debugging much easier
And with something like Mypy, you can get most of the benefits of static typing even in Python, if you want to. But Rust has so much more to offer, such as memory ownership.
Python will actually give you an error for `print foo` as soon as it parses the file. But there definitely are other scenarios where you'll only get the error in the middle of execution (such as passing the wrong type of thing as an argument to a function)
Agreed but Python lazy-loads & parses files as packages are imported. By convention these are all at file scope and at the top of the files, so it's often confined to an initialization phase. But...sometimes developers use clever fallback behavior by catching ImportError.
So the scenario described is possible to escape simple tests, I suppose.
Yes, sorry if that was confusing -- perhaps I shouldn't have brought up ImportError. The point was that uncovering SyntaxError could possibly be as challenging as other feature tests.
Use a linter. Most IDEs will do it for you natively, and even if you're dealing with a terminal based editor like vim, it's really easy to get the output of pylint, flake8 etc. running in it. It's extremely rare I ever get a syntax error make it through to code review, let alone build/deploy.
It's nice to see a balanced, real world, case study including 'these things are fixed by Rust', 'these are problems that don't occur in idiomatic Rust', and 'these are problems that Rust can't help you with'.
I'm a big fan of Rust, but the one sided 'Rust makes all the problems go away' articles don't provide any value.
It also highlights an example of a security bug introduced during rewriting; highlighting that rewriting any significantly large piece of software is bound to introduce bugs.
Not just introducing new bugs but reintroducing old bugs which were publicly documented and/or previously exploited. Which you could argue are worse as it’s a lower barrier for detection by attackers, but also on the otherhand by the team/community.
Also of note was that there was already an automated test for one of the high priority bugs that got reintroduced but the that particular tests was turned off.
What confuses me about this is the tests were turned off because they were taking too long. But wouldn't the appropriate behavior there be "run a subset of the tests normally, but run the full test suite occasionally" rather than just disabling the tests completely?
These are all ways that Rust is neutral or better. Were there any ways that Rust was worse? For example, did the previous code use value-type templates? If so how was their absence worked around in the new code?
They're still using Rust for some stuff, just not for the game itself. Wargroove's matchmaking server is still in Rust. That said, yes, this is a good example of what your parent is asking for.
Compile time. For my 9k lines project it takes 20 sec to compile in debug mode and 1 minute in release. Wich is insane because I have to do a lot of runs to test things(where cargo check can't help).
I tried to change some constants in random places(in 1 line each time) and on average it's ~10 seconds
Also compilation unit in rust is a crate. I guess if your app can be separated in different crates it can ease the pain.
Overall, it did a decent job of being balanced, but I don’t buy the memory overflow example at all.
For one thing, idiomatic C++ bounds checks by default. You need to use at(). If you don’t like typing at(), you can implement an array type that always bounds checks fairly easily. On that note, the vulnerable c++ code should be using accessors, not indexing to access the oddly packed and laid out array. Even the fixed version wouldn’t pass a code review from me. You could write equivalently bad code in any language that supports array types, and get similarly broken results.
For another thing, there’s no evidence that you couldn’t achieve the same improved data structures in C++ using its type system (which is turing complete...)
The “thread safe by default” property sounds interesting; I’d be interested in reading more about that.
> For one thing, idiomatic C++ bounds checks by default. You need to use at().
Sounds like it doesn't check by default then. It checks if you remember to check using the more verbose bounds-checking method. Not unlike the issues with subscripting std::map.
I have some experience in C++, and I am familiar enough with the standard library to remember that operator[] doesn't check bounds while the at member function does. I would assume that the Firefox C++ programmers know this as well. However, maybe I'm wrong or have too much faith?
Or, maybe, the programmer was aware that operator[] didn't perform bounds checking, but opted to use it for some reason? A good way to dissuade people from making unidiomatic choices is to make them more verbose. IMO calling the at function isn't particularly verbose, but if the member function that didn't check bounds were called something like "at_unchecked," perhaps people would be less inclined to use it.
Also, from the snippet in the blog post, note that you can't tell whether the Firefox code used std::vector, C-style arrays, or some non-STL container type. Projects may use their own container types, but your criticism only applies if the programmers were using the C++ standard library.
> I have some experience in C++, and I am familiar enough with the standard library to remember that operator[] doesn't check bounds while the at member function does.
Everybody knows you're supposed to check pointers for being null, and yet time and time again developers fail.
As long as you rely on human nature and provide one API which is simple, convenient, obvious and dangerous and one which is complex, inconvenient, non-obvious and safe, you will just drive users towards the former.
> I would assume that the Firefox C++ programmers know this as well. However, maybe I'm wrong or have too much faith?
Just because they know when quizzed doesn't mean they'll always remember when actually doing. Even less so when subscripting is safe in pretty much every other language which provides array subscripting, and ::at… only exists in C++?
> IMO calling the at function isn't particularly verbose
No, but it's still more verbose and less intuitive than [], especially given the above (that tons of languages use [], and very few have an at method)
> A good way to dissuade people from making unidiomatic choices is to make them more verbose.
The relevant code in fact uses a non-STL array container. Unfortunately, the performance of the STL containers is fairly unreliable across C++ standard library implementations and can be very poor in some cases. That makes them harder than it should be to use in code where you need to understand the performance characteristics of your data structures.
Huh. According to https://en.cppreference.com/w/cpp/container/vector/operator_..., “no bounds checking is performed.” I find cppreference.com generally trustworthy, but maybe it’s wrong here? Or, maybe “no bounds checking” actually means “no guaranteed bounds checking?”
It means “no guaranteed bounds checking”, the standard only requires at() to throw if out of bounds (§ 26.2.4.1, note 15), but leaves unspecified how operator[]() should behave in invalid accesses, only that it isn't allowed to throw.
You need to look at the text of the Standard itself when it comes to this level of language lawyering. The short answer is that operator[] has undefined behavior if the index is out of range. Performing a check and terminating the program with some kind of runtime error is a legal subset of "undefined behavior", and so it's commonly done in debug builds, but you cannot rely on it in any sense.
This is like saying the Rust doesn't have memory safety because you can use "unsafe". C++ offers you a safe Api and an unsafe API if you want to go that way. If you decide to use the unsafe api the consequences are on you, the same way that it is on you if you decide to use "unsafe" in Rust.
The difference is in the defaults. In Rust, the easiest and more common thing ([]) is checked, and the more verbose and uncommon thing (get_unchecked) is unchecked. In C++, it's the reverse.
> This is like saying the Rust doesn't have memory safety because you can use "unsafe".
It's the exact opposite which is the point. Rust requires extra work and using the non-default and less convenient way for unsafety, C++ requires extra work and using the non-default method and less convenient way for safety.
> You could write equivalently bad code in any language that supports array types, and get similarly broken results.
You wouldn't get "similarly broken results." The results for doing this in C/C++ are far more serious, which was a point the article made.
If you do this in Rust/C#/Java/etc they will safely crash. If you do this in C/C++ it is undefined behavior, it may crash, but it also could allow remote code execution.
The Rust version of this bug is Security-Low (crash), the C++ version is Security-Critical (potential RCE).
Right, but all you have to do in C++ is switch the array implementation to bounds check by default. This is not rocket science, and is certainly easier than rewriting large code bases from scratch.
Similarly, I could complain that rust arrays are too slow, and produce an array implementation that uses unsafe under the covers.
> The “thread safe by default” property sounds interesting; I’d be interested in reading more about that.
This property is simply a corollary to Rust's ownership invariants enforced by the borrow checker. There can only be a single live mutable reference to any object, therefore two threads can never hold mutable references to the same object simultaneously. Similarly, all objects must have a lifetime at least as long as the objects which reference them. Therefore no thread can ever hold a stale reference (mutable or immutable) to an object. If code passes Rust's borrow checker it must necessarily be thread-safe.
The borrow checker doesn't need any special knowledge of threading, though AFAIU there are traits that permit the compiler to check that you're using the correct boxing type when passing objects to threads. Objects which implement these and other traits are responsible for maintaining ownership invariants using unsafe code.
TL;DR: Threads cannot share mutable references because no code can share mutable references in Rust. It follows that any Rust code is thread-safe, ignoring bugs within or induced by unsafe code.
This is also the normative approach to writing "thread-safe" code in Unix programming more generally. Most libraries that grew from the Unix culture are only written to be re-entrant--they never hold references to objects shared outside their encapsulation or execution scope. Therefore most such libraries claim thread-safety provided that callers maintain the same re-entrancy invariants for library-defined objects. Without needing to use mutexes such code is thread-safe, you just don't normally get compiler verification. Contrast that with Windows programming or, especially, Java, where the expectation is that objects are shareable and guarantee thread-safety internally. Depending on which programming culture you grew up in, Rust's thread-safety is either obvious ("oh, it's just enforcing re-entrant-safe APIs") or magical ("how does it insert locks in all the right places?").
> Contrast that with Windows programming or, especially, Java, where the expectation is that objects are shareable and guarantee thread-safety internally.
That's not an expectation on either. For example, the single most common phrase you can see on MSDN in class docs is: "Public static members of this type are thread safe. Any instance members are not guaranteed to be thread safe."
There was a period when it was different for some things. In particular, both Java and .NET had thread-safe standard collection classes initially - e.g. Vector in Java, ArrayList in .NET. This has proven to be a bad trade-off in both cases - it's a massive perf hit for something that's not even all that useful even to threaded code, because in practice you often need to perform multiple operations on the collection atomically, and then you still need your own lock.
So they have since been obsoleted by new collections that do not attempt to do any thread synchronization; the old collections remain for backwards compatibility purposes. Modern idiomatic Java or C# code doesn't do any kind of synchronization or locking to protect the caller, unless that is specifically the purpose of this class or function to provide such things.
So I wouldn't say there's significant cultural differences between Windows, Unix and Java in that regard.
That's not "by default". std::array_view still hasn't landed so I can't even wrap ptr+size s provided by third party libraries or across standardized C ABIs without going beyond the standard library (really not by default now)... and I believe I have yet to see a single solitary use of at() in a production C++ codebase. Some "default".
For code where overflows are expected, C++ exceptions are way too heavyweight and some other kind of "attempt dereferencing" pattern is used (if only checking the index manually before invoking operator[]).
For code where overflows are unexpected, uncatchable assert-style checks are used.
I've heard these "idiomatic C++" and "improved data structures" arguments a few times and as a C++ developer, I'm a bit skeptical. Does anybody know of any non-toy C++ projects that actually demonstrate such a high-level of reliable use of C++?
If so, how much do they rely on the developers being ideal programmers, who know intimately the intricacies of C++ and how to avoid ending up in UB-land?
I have a codebase that amounts to several kLOC of C++, available here: https://git.sr.ht/~maelkum/viuavm While I openly admit that parts of it are shoddily written, I also try to always use the "reliable subset" of C++.
From my experience it is not hard to make it a habit (e.g. using `::at()` and declaring variables as `auto const x = ...;` is muscle memory at this point) but the code becomes very verbose and looks like programmed defensively to a sometimes ridiculous extent.
So I understand your skepticism. To address the point of relying on developers being "ideal": just turn the compiler warning flags up to eleven, make all warnings errors, and run your tests two times - one time with sanitisers, and the other under Valgrind. This won't catch all errors, sure, but will still make you more confident in your code's reliability.
I'm pretty sure OP meant to write "idiomatic C++ does not bounds check". That's certainly true - the only place I've ever seen at() in use is in textbooks, never in production code. It doesn't really make much sense, because the moment you start using iterators, it's all unchecked anyway, so why bother with at() specifically?
Yeah I keep hearing / reading about rust, even seen some demos but the demos all end with "oh no I'm not using this for anything". Still cool but ... want to see someone doing something in production / get their thoughts on that.
Edit: To be clear I'm not saying anyone isn't using it, this is just more of a comment about the impression I can get when I hear about X tech is so cool, but that's most of what I hear and at some point I want to see those same articles about real world use / experiences. I'm aware Mozilla and others are using it.
>Yeah I keep hearing / reading about rust, even seen some demos but the demos all end with "oh no I'm not using this for anything". Still cool but ... want to see someone doing something in production / get their thoughts on that.
Huh? Besides Mozilla itself using it in the browser in several backends, there are tons of places where its used in production (and several articles on HN on the topic).
Where do you see all those demos who say "I'm not using this for anything"?
I've seen articles on Mozilla using Rust, Dropbox using Rust, MS using Rust (rustgrep used in VSC but also something in Azure IIRC), Google using Rust (Google Fuchsia), Facebook using Rust (Mononoke, etc), and so on.
I'm using it for an not-yet released product. Roughly 30k lines of code running on Windows, macOS, and Linux. Mostly high volume event processing and parsing.
Been a lot of fun to work with. Having spent 10 years doing C++ professionally before, I can't see myself ever looking back.
Once product is public later this year, I may see if I can talk more publicly about it.
We're doing a low level core functionality lib in Rust that is shared between Windows desktop, iOS and Android apps.
It works really well.
If we were to do it again, the core Rust lib would do much more and the platform native code would only be things that need to be like UI and notifications.
Our product is basically written in Rust (~40k LoC) [1]. When I say basically: there are a few backend components that are written in Java (i.e, ActiveMQ) and the frontend is TypeScript/React, but it is mostly Rust.
I'm not saying nobody is using it, just the volume of "hey this is so cool" gets to be a bit much and I want to see more "here it is solving problems in production for a real thing".
The explorations and small little demos and projects in a language is almost always higher than the number of production uses, it's just that in popular languages nobody cares to read much about it (or at least share is so it spreads), because it's already known. Rust is still fairly uncommon, so you're bound to see a lot of people announcing their experiences while playing around with it, and that can skew the perceived ratio of things.
I think the problem is solved by paying less attention to the stuff you care about less, and/or seeking out more production(y) uses if desired by looking in places where it's already common, such as /r/rust, where the content ratio will naturally fall more favorably towards production uses, libraries and advanced discussions since a minimum level of experience (or at least interest) can be assumed.
I primarily use Java for my job. Security and memory related features of Rust are not an advantage compared to Java. But I like rust because it feels modern and produces efficient standalone binaries. Most of my hobby projects are in Rust now. But I would not rewrite any of my work projects in Rust even though they require ultimate performance. That would be a maintenance burden.
It is great to see people rewriting in Rust where it makes sense.
For me, also primarily a Java person before Rust, it’s datarace safety that captured me.
Java’s executors improve the world, but I can’t count the number of concurrency bugs that I had in Java that are not possible in safe Rust. This isn’t just my code, btw, but large teams where it’s hard to disseminate good practices when building threadsafe code. If it had been in Rust, those issues wouldn’t have happened.
Other things that I appreciate about Rust over Java is the error handling combined with RAII, doing away with nasty bugs around try’s lacking proper finally statements for closing file handles, etc.
Java has its warts, not everything is just about memory safety.
I think the best way to look at it is that memory usage becomes predictable and GC pause free. An application of similar scale and implementation between Java and Rust won't necessarily use less memory when in Rust.
I use Java constantly in my job and recently tried rewriting a math & memory heavy component in rust to see what performance gains there might be. Surprisingly (to me) the naive rust version was ~15% slower than Java. There’s probably room for more rust optimization but it was interesting that “efficient standalone binaries” doesn’t automatically mean faster too when competing with HotSpot.
I find where rust usually shines the most is if you do text processing. String allocations take time. In rust you can often avoid them and use things like cow to only allocate when you change something. That way often my text processing heavy scripts go twice the speed of a C++ version, and they're easier to write with Cargo, too.
Compared to highly optimized Java and C# I could often get a quite naive rust implementation to be 10x faster.
Naive rust means I didn't spend much time optimizing but I do use appropriate algorithms and to avoid unnecessary allocations.
My experience working on writing image processing code in Java is that rewriting the slowest bits in C++ only gave about a 10% performance boost, and this was over a decade ago. Moving stuff to the GPU was vastly more effective than doing faster CPU work.
When comparing speed on the JVM with speed with native languages, the only positive side you get from native binaries is cold start nowadays.
For a webapp this doesn't matter, but as we're moving towards more cloud functions it start to make a lot of sense.
That needs to be ponderated by the fact any real life application will have to access the network at bootstrap to load configuration and therefore your bottleneck will most likely be I/O.
Huh! I have a fairly strong vocabulary and thought this might have been a made-up word -- but apparently it means "to weigh down or give substance to", which aligns with your intended point here. TIL
English is my second language. I did a quick google search just to make sure it was correct but it's hard to gauge if a word that happen to be in the dictionary is also widely understood.
I remember reading an article about Java performance many years ago. It had an example for Java being faster than C++ and it was maths on arrays / matrices. I guess that kind of code least trips up HotSpot optimizations, so it wins due to availability of runtime information and a really good implementation of certain parts.
Though I doubt that Java usually wins against the Eigen C++ library. Eigen uses some template tricks to fold operations together (which can avoid intermediate storage and unlocks more types of restructuring optimizations) and uses SIMD extensively.
I write Java daily, I haven't seen one in a production environment in... at least recent memory. Most common NPEs I've seen can be resolved by doing two things: 1) for returning a single, nullable value, wrap it in Optional. 2) Never return null collections, always default to an empty collection instead.
More nuanced ones can often be caught by using all args constructors and requiring the constructor values with Objects.requiresNonNull() or similar. Using spring? Don't use field or setter injection, always constructor so the above applies as well. Making the state of your objects largely immutable means their state is more consistent and what is null and when becomes a lot less surprising.
Lastly, write good tests. If you're exhausting the behavior of your system with tests, these things are much less likely to surprise you later.
NPEs are definitely a problem with Java, but they're a very avoidable one as well.
Edit: I don't understand the downvotes. The parent said they're a huge deal and I'm disagreeing because I think they're relatively simple to avoid?
Most issues in software fall into this category, though. The issue with Java’s null is that it’s not type safe. The language is generally a strongly typed language, except in the case of null. And until Value Types land, there is no way to express nullability to the compiler.
The arguments you make aren’t all that different than the undefined behavior arguments with C. “Just follow these simple rules and you’ll avoid all issues”, a compiler should do that for you IMO.
I can’t wait for Value Types in Java, at that point we can have real Option types.
No downvote, but GP post conflates "how code SHOULD be written" with "how existing code was written" in my opinion. Same with one of the earlier comments about "idiomatic C++ does bounds checking." I've seen plenty of NPEs in real, working Java.
If Mozilla isn't managing to write idiomatic C++, I don't hold out much hope for myself either.
> “Just follow these simple rules and you’ll avoid all issues”, a compiler should do that for you IMO.
This was one thing I really liked about Kotlin vs Java. It's nice that my IDE can do the work of generating getters/setters on a POJO for me, but it's not nice that the language requires me to fill my source code with that noise. To a first approximation, any task rote enough the computer could do it for me, it should not ask me to do.
See also: static languages that don't offer type inference.
this is just an honest question, and i’m genuinely curious, but why couldn’t Optional<T> be done as an immutable reference type... why does it need value types to be implemented first?
Can also use checkerframework to statically analyze your code for NPEs. It effectively augments the type system to require you to handle nulls where appropriate. It doesn't recognize I/O boundaries for you, you have to make sure those are annotated properly, but after that with sensible defaults, you can be more confident of the NPE-free-ness of your code.
I suppose that you are being downvoted because, though your arguments are correct, this thread is not about "how to avoid NPEs in Java" and is therefore OT.
That said, "write good tests" is impossible. You can write as many tests as you want, you can make sure those loops loop or don't, and those ifs if or don't, but you cannot write "good" tests. Tests will always miss some corner case, some input, some unexpected locale variable.
In my experience null pointers and unassigned variables were a big problem in C an C++. But in java? They were never something that really caused a major issue. Most of them turn up in tests and are really easy to fix because you get an exception stack. Very few turn up in production and often it is because something else failed at runtime to cause it. If the null pointer wasn't there you would have had to deal with that other failure anyway.
I haven't written any Rust in a few years, but last I knew, it was very possible for a Rust program to crash. The guarantee is that it won't corrupt memory while doing so.
That's true, but 'null pointer exceptions' (which I suppose means expecting a Option<&T> to contain a &T when it is instead empty) are much rarer since nullability is made explicit.
Rust goals in life towards bugs are basically
- Isolate memory bugs to unsafe code which you rarely write.
- Make as many classes as bugs reasonably possible rarer by encoding as much as is reasonable in the type system and encouraging the programmer to think about all possible cases.
The first gets all the attention because it is the one you can make guarantees about, but the second is really just as important.
It is possible, but the causes and frequencies are quite different.
In Rust runtime panics are certainly possible (array out of bounds, out of memory, etc), but the ones analogous to Java's NullPointerException have to be explicit opted into (unwrapping optionals) and do not happen implicitly.
Rust lets you handle optionals in nicer ways which lets you be sure you covered the "null" cases at compile time with no runtime panics possible, if you like.
I think that is somewhat related to the article. There a class of bugs not possible in Rust, they are also not possible in my GCed langs. unwrapping an `Option` is similar to NPE. but that code does not feel idiomatic Rust. IMO there is higher chance of making NPE mistake in Java than unwrapping None in Rust for similar logic.
There are different reasons why a program can crash. Nullpointer exceptions are one of them. Out-of-memory situations are another. Rust protects you against the former, but not the latter. But at least in my experience I only encountered crashes when I disabled safety checks by calling .unwrap().
unwrap() does not disable safety checks, it just means you ignore/disregard error handling. It will panic and abort the program, but not cause a safety issue.
This is actually an interesting distinction. The way that Rust defines "safety" is a little jargon-ish. It's probably more useful than the (highly impractical) colloquial meaning, but still different. In a rust context, "safety" means that the program is protected from a very specific set of things.
One might expect that set to contain things like crashing (panic! and friends) and leaking memory (forget). It does not, and is not intended to.
Not exactly "disabled safety check": `unwrap()` is an assertion check (that may sometimes be optimized away by the compiler). You're still checking safety, but you're doing it in a much more terminal manner.
Wouldn't using Koltin or Scala, which benefit from the engineering effort of JVM and arguably its ecosystem solve this problem ? (Since NULL is not idiomatic in either language)
Yes and no : I have been using kotlin for a while for Android.
Currently our codebase is 75% kotlin.
Issues arose at the interface between java and kotlin. Unless there are @Nullable @NonNull annotations (and they need to be truthful), the kotlin compiler cannot know the nullability of something coming from a java method.
It can be pretty pernicious : if you use some java written libraries like Moshi (json parsing), it can also lead to crashes : IIRC if you declare a moshi generated property to be non null but it is absent from the json, it will generate an object with null, creating a crash.
Still, null is now an anecdotal issue in Kotlin. The Android framework team is working on annotating all their APIs with the corresponding nullability annotations and more and more JVM libs are also working on handling it gracefully.
It was never a huge issue to begin with even in java, just pretty cumbersome to have to add annotations everywhere and have to add some policies like 'no null collections, only empty' in a codebase to have sane handling.
It's not idiomatic, but it's still possible. I've got NullPointerExceptions while writing fairly complex, modern Scala, so it's about as possible as with Java in my experience.
Rust is often sold feature by feature; the borrow check offers proof like safety over fuzzing, cargo provides real versioned package management over makefiles or git commits...
I choose Rust because taken as a whole, Rust changed the way I approached laying out my memory and how I composed my code. I think this more than anything leads to less issues than the equivalent C++. The article points out that a Rust vs C++ solution to any given problem are going to be completely different.
My only desire for Rust is to see compile times speed up and the C++ interop to improve.
One of the major hurdles in rewriting parts of C++ projects in Rust is that the interop surface between both languages is C. The necessary interface layer has created more bugs and work than the conversion saved. I'd really like to see more high-level interoperability between the two languages in the future, although C++ is a pretty fast-moving target at this point, with all the changes in C++20.
But what would you put in such an ABI beyond what's in the C ABI? Beyond basic data types, struct defintions, and function definitions, languages begin to wildly diverge almost immediately.
More challenging would be to include stuff like allocation management (how to free a pointer when it's done), GC integration, function boxing, iterator/generator support. Vtables and cross-language inheritance is interesting but difficult.
One point of clarity: this would be an FFI ABI, not necessarily expecting that most datatypes would be laid out according to the ABI.
> The necessary interface layer has created more bugs and work than the conversion saved.
Is this from a project you did? This is weakness, for sure, but I'd be interested in hearing more about why it failed for you! We have some people working on this.
This was my experience when I was at Mozilla converting a part of Firefox to Rust. It also feels like a lot of Rust's safety guarantees go out the window when you are using the C FFI to talk between C++ and Rust. I'm not sure how bad it actually is in practice (vs just writing the whole thing in C++).
That's a proportional problem at least right? As months pass the more of your libraries converted to Rust the lesser the problem.
The potential party spoiler being third party code that's not practical to replace. In some cases even decades won't change it's nature, as a few examples have shown.
In Rust, or any statically typed language such as C++ or Java, the idiomatic way to handle untrusted input is to treat it as a "bag of bytes" before you access it. Then either parse it into a strongly typed object or bail out of parsing. The strongly typed object is safe to use. Bailing out (throwing an exception or returning an error type) does not allow the program to continue assuming that the (malformed) input was correct.
More to the point, you should put untrusted input into a different type from trusted input. As much as I admire the design of the servlet API I think the biggest mistake is that everything is transmitted as Strings. The input characters should have had a different type than the output characters.
That's a feature that might work well with some plausible ways of guaranteeing memory safety in a type system because object reachability is a form of taintedness.
Recently some colleagues started using the type annotations in the latest python3. Really excited for this feature! It’s going to make a lot of our production systems safer to work with.
Can you build in some kind of "unsafe release" mode, so that every array bound check that were asked in the code are skipped ? If not, would it be an interesting feature ?
No. Such a thing could only remove some kinds of checks; for example, if you see that code sample later in the thread with a manual check, it wouldn't know that's what you're doing.
In general, we don't want to make it easy to turn checks off. They get removed if the compiler can prove they're not needed; if they're there, they're almost always for good reason.
You could do something with rusts feature system and macros. In essence you'd have a macro that would run a different line of code if your feature is enabled versus disabled, so you could use the unbounded lookup on the array.
That said, this would be a user implementation and wouldn't be likely to be provided by the standard Library
We totally tried to write this twice then we switched to language x and everything is great.
Feels like something a language zealot would say. I would scoff if someone at my company rewrote a core section in a different language.
It is their language so maybe they just told them to do it that way ha.
This comment is confusing two completely separate concepts: zero cost abstractions and run time bounds checks. Rust does provide zero cost abstractions: if you do something explicitly/manually instead of using the abstraction you get the same generated code.
If you want to combine the two concepts, I guess you could go for an example like this:
if index >= vec.len() {
panic!("out of bounds");
}
let value = unsafe { vec.get_unchecked(index) };
which can be rewritten using higher level abstractions as:
let value = vec[index];
The second is more abstract, and that abstraction is zero cost, you do not pay for using the abstraction more than the explicit code above it.
For that specific example it's hard to be expensive but here's a loop from one of my crates that compiles down to efficient code:
pub fn decode_12be(buf: &[u8], width: usize, height: usize) -> Vec<u16> {
decode_threaded(width, height, &(|out: &mut [u16], row| {
let inb = &buf[(row*width*12/8)..];
for (o, i) in out.chunks_exact_mut(2).zip(inb.chunks_exact(3)) {
let g1: u16 = i[0] as u16;
let g2: u16 = i[1] as u16;
let g3: u16 = i[2] as u16;
o[0] = (g1 << 4) | (g2 >> 4);
o[1] = ((g2 & 0x0f) << 8) | g3;
}
}))
}
The "decode_threaded()" call is a function call that passes in a closure with the inner loop to a generic function that is used to multithread a bunch of similar decoders, instead of repeating that code. And the for loop is actually describing what I want (process every 3 bytes into 2 output values) instead of having me manage some iteration variables and have off-by-one bugs. "chunks_exact" and "chunks_exact_mut" are recent additions that allow me to say that I only want to receive exactly 3 bytes and output exactly 2 values, so if the array is improperly sized the extra at the end just gets skipped. This not only matches the intention of this code (I only ever process 2 pixels from 3 bytes and nothing else will work) but also gives the compiler a better way to lift the bounds checks out of the inner loop and make the code significantly faster (by 2x in some decoders).
Now let's see "decode_threaded":
pub fn decode_threaded<F>(width: usize, height: usize, closure: &F) -> Vec<u16>
where F : Fn(&mut [u16], usize)+Sync {
let mut out: Vec<u16> = alloc_image!(width, height);
out.par_chunks_mut(width).enumerate().for_each(|(row, line)| {
closure(line, row);
});
out
}
Besides allocating the image it uses "par_chunks_mut" to make the code threaded. That's a function that's provided by the rayon crate which manages a threadpool and it's scheduling for me. So I'm even using code written by other people, to build something that's apparently deeply nested even though it needs to be fast. And yet after all this indirection and syntax goodies the end result is efficient machine code that matches or is even better than the original C++ code and not the dynamic runtime like behavior of Ruby/Python that you'd expect from such code.
That's what zero cost abstractions are. After memory and data race safety I think rust shines because it gives me a lot of the ergonomics of Ruby with the speed of C.
Right, and this is equally true in Rust and C++ (and any other systems language). Zero-cost abstractions doesn't mean that the compiler is imbued with magical properties.
And there's your wide execution width making it possible to do the checks in parallel with the access. You are using up entries in your branch prediction structure when you have something like that, as well as instruction bandwidth, but I'd doubt the function will run any slower once past decode.
It is. As you say, there's a category of Spectre bugs where the CPU speculatively assumes a bounds check succeeds, then goes on to execute attacker-provided code.
"C++ implementations obey the zero-overhead principle: What you don't use, you don't pay for".
In Rust you pay for e.g. bounds checking or integer overflow handling or optionals.
But I don't understand why everyone's getting so defensive about this, since it's the only way (static verification and proofs aside) to get the desired safety characteristics...
In Rust release builds the integer overflow of checks are disabled. They're not zero cost and there's no magical way to do it, so it's only done in debug builds.
You can disable them in debug builds as well with wrapping integers. So this is again a practice of making the safer option the default, but won't incur runtime costs if it bothers you.
If there is a way to do it at compile time that will usually be used in rust.
> Rust requires a lot of runtime checks, but that's the price one has to pay for memory safety.
It requires a lot of runtime checks on the boundary between rust and C/C++ code. You cannot trust C/C++ code, so you are forced to check everything. C-function returns a enum value? Check that this value is in the enum, before converting it to the rust enum. Check that there are no situations like
But after you've done all that checks you need to check almost nothing, because if you have a enum value in Rust, you know for sure that it is a valid value. And compiler knows that the value is valid and optimizes accordingly. If you have an &str, you know that it is a valid utf8, you need not to check it on each access, it have been checked already. You got a valid pointer from C? Wrap it into NonNull, so neither you, nor compiler would need to check validity of a pointer once more.
It is unclear thing, who needs more checks -- safe code or unsafe code. I'm gravitating to a belief that unsafe code needs much more runtime checks.
If your use iterators instead of indexes, you have no bound-checks because they are optimized away. I've been working full time with rust for a year and I'd say I need indexes less than 10 of the time, most of which are for fixed-size array with constant indexes, for whom the bound checks are also optimized away. So I'd say they aren't really a problem in practice 95% of the time. If you really need to go unsafe for the last 5%, well you're still 95% safer than C++ :)
Even in that case it's not pointless. You can do a lot of testing and fuzzing with checks enabled to get some confidence that you don't have bugs. Then disable the checks for performance. That's just one option. I think it's nice to have options even if I choose not to use them.
Even better is to use iterators and other abstractions that don't require bounds checking at all. Rust has lots of good tools to build efficient code.
The article is arguing that Rust somehow has better capabilities than C++ to fight memory-related bugs, but the example vulnerability given is not something Rust can solve nor is more powerful than C++ in its “bug catching” capabilities regarding this kind of bug.
Concretely, the article claims that in Rust the vulnerability doesn’t become a bigger problem because it simply crashes at run-time due to built-in bounds checking. True, but that is alao the case as well with C++ if you were using the equivalent Vec type with mandatory bounds checking - which many projects do (and, critically, enforce).
Personally, I like what Rust brought to the compiler/language world. However, some people is definitely overstating the case. Most non-trivial memory-safety errors and vulnerabilities are related to runtime problems like the example shown. In these, no language can help in the general case - we are not solving the Halting Problem. Therefore, saying Rust is immune to memory-related problems is not true. It is true, however, that those bugs will not trigger anything worse than a crash if there is no unsafe blocks. The same way that many other common languages out there do (Java, C# and many others).
The same way, I have seen people (and even the linked blog) to claim Rust is free of race conditions or thread-safety issues (even if it introduced great ideas to write correct code).
Giving a false sense of security is the worst thing we can do.
It is not realistic either to ask everyone and every company to rewrite all their C/C++ code in Rust. Even if it were financially doable and a rewrite were to happen, in many cases it would simply be best to move to a language like C# anyway, not Rust; for productivity reasons. Where performance allows, of course.
In my opinion, the realistic and pragmatic solution is, instead, to strive to make all languages (in particular C and C++) embrace security-first approaches/types/mechanisms like Rust does. The compiler tecnology is already written - now retrofit as much as possible into C++ (even to the point of introducing a “safe” scope if needed) and allow companies to embrace it at minimal cost and progressively.
A system crash is a bug. Period. In many cases it could lead to Denial of Service. An insulin pump can stop working.
I remember when C# came out almost 20 years ago. People said "I can forget about managing memory so I can focus on the logic". Programs kept crashing, memory problems were still there.
The article goes with "...remove the burden of memory safety from our shoulders, allowing us to focus on logical correctness and soundness instead...". More or less the same, and admitting that said problems won't go away.
But here we are, it's 2019 and we're still using C/C++ as if nothing happened.
That's still a bug. I feel like you're being intentionally obtuse about what's considered to be a security problem in code. Nobody ever suggested that rust code can never crash or have bugs. It's just about memory safety, which obviously has nothing to do with door locks.
Sorry if I gave that impression. It was not my intention.
> A crash is a bug but not a security problem.
I think that all bugs, the ones that produces crashes and security bugs should be all treated equally. A bug is a bug, whenever it has security implications or not.
To me, the article gives the impression that a system crash is not a security problem, because a Rust program will "terminate in a controlled fashion, preventing any illegal access". But one for example, can fingerprint a system by forcing it to crash.
And of course, nobody expects that Rust will prevent bugs from happening, but at the same time I don't get why the fixation of setting a difference between security bugs and bugs.
But the idea that I don't have to think about certain classes of problems allows me to give these stupid things more focus, which is surprisingly refreshing.
The predictable nature of Rust was so refreshing for me that I ended up using it even for smaller reusable scripts where I would happily have used Python before but soon got annoyed with obvious errors that would only show up once you run a program.
If you e.g. have a `print foo` in some obscure branch that rarely happens, that print will ruin your day if you use Python 3. If python would be a little like Rust you would get on save (or at least on compile) a hint or error, that the print should look like this: `print(foo)` for Python 3. You can be incredibly careful and rust will still catch things now and then, that would have gone unnoticed into production unless you have immense test coverage.
I like Rust for the experience I had with it. It definitly changed how I approach certain problems in a very good and productive way, even when I don't use it.