Why Rust, or a Trip Report from My Satori with Rust and Functional Programming
christine.website
christine.website
Go's equivalent to Rust's Vec should have simply been []int64 instead of using list.List (which is a doubly-linked list, aka std::collections::LinkedList in Rust). And yes, I read the blurb at the end where they say they know slices exist. The issue is that they proved nothing useful by not using them. You don't need to recreate how slices work because _they already exist_, and is well-known it's all currently compiler magic (until they introduce generics).
In the JSON unmarshalling example, the reason you need struct tags in Go is because the JSON field name is different from the one given in the Go code. You'd have to do the exact same in Rust if they had named it similarly to the Go example (https://serde.rs/field-attrs.html).
The other stuff about catching concurrent issues is true, but working with concurrency in Rust is a testament to one's patience as well (especially when working with multiple processes/threads), so you're trading off usability with safety—and to be frank, it's pretty easy to remember that the only things that work concurrently are either atomic, use locks, or some sort of queue.
Some languages are dynamic (Lua being ht example here but obviously there are many others eg Python). The author then chooses to differentiate types of statically typed languages to those with generics (Rust being the example) and those that don't (Go being the example).
I'm not sure that's a useful distinction because all type systems have limitations and there are many orthogonal dimensions. A language can have (or not) generic types while separately being able to define enumerated types (or not), as just one example.
The interesting thing (at least to me) about Rust is that Rust goes beyond describing types to describing (at compile-time) ownership of data. There is a developer cost to this and a compile-time cost. Personally my view is:
- Compile time errors are cheaper to debug and fix than run-time errors. That's the whole point of having a type system: to catch errors at compile-time. Better that than a segfault. Rust is novel in that it eliminates a whole class of problems with data ownership, lifetimes and access. Not every problem of course.
- Memory-safety is just too important such that Rust will increasingly fill a niche because this is just too easy to get wrong in, say, C/C++.
You do have to jump through hoops go properly describe ownership semantics so the Rust compiler can satisfy safety constraints. Like a type system, there are limitations to this and you can intuitively come up with examples that you know are safe but are hard if not impossible to describe to Rust. Rust does get better with this as time goes on.
Whatever the case, I'm very bullish on Rust.
It no doubt that the barrier to reading / learning Rust is still higher compared to other languages despite the languages safety guarantees and performance. It is still a new language that some large companies can afford to invest their time using it. However if a senior dev that pushes for a rust codebase and ends up leaving the project, the team would probably revert back to the second-best language that everyone knows rather than continue. It doesn’t make any sense to use Rust for the sake of Rust or even strangely: “because everyone else is using it”.
Even as a Rust user myself, the author has failed to mention about the quality of the crates ecosystem which mostly still have pre-1.0 libraries and aren’t even stable yet and even a popular few had and still have unsafely issues.
In a typical production software house of 2025 for new server-side software, I would use Rust over C++ and JavaScript (Node). But for 2020? Kotlin/Kotlin Native sounds like a safer bet for some companies over Rust due to leveraging the existing Java ecosystem and being a safer alternative to Java.
You should consider plain Java tbh. Java 15+ is basically Kotlin anyway at this point. It's much easier to hire or outsource Java developers.
Are you aware that there are outsourcing projects doing Haskell, Scala and C++ as well.
Hint, the deliverables aren't usually what one would expect from the skill levels usually required by average users of those languages.
As for the second example, it's more of a complaint about the library. There are libraries that enforce stricter checking. Or you can code gen or roll your own deserialization logic.
> but then the job of ensuring the sanity of the inputs is shunted to runtime rather than stopped before the code gets deployed
It's always runtime, even in Rust. You get some blob from the network to deserialize, it will return an error if it's bad, just like in Go if you try to Unmarshal bad input to a struct.
Seems like people think Rust has this magical ability to prevent errors. Big expectations lead to big disappointments.
How "stable" is Rust, libs and tooling ecosystem though.
Regarding the main example of author, it's great that the progressive side of Go team won, so generics are coming in Go 2.
Java, C# and C++ control it via compiler flags, and also have the ABI issue across language versions.
I like rust's syntax. It has very elegant macro syntax, expressions are values, and all that other good stuff.
There are some small parts of it that are ugly, but honestly Java and C++ are far uglier. Templates in c++ are awful. Java's wildcard syntax and lack of macros is ugly.
I find rust much prettier syntax-wise than most statically compiled languages. Prettier than Go or Java or c++.
> In terms of ease of getting started and using tooling, it really can't ever compete with the corporate languages like Go, Swift, Kotlin
Why not? I don't see any reason why it can't compete. Heck, it's a pretty corporate language too in terms of its mozilla backing.
That being said, people constantly talk about how easy it is to get started with python, and python has even less corporate backing than rust. That point makes no sense to me.
> C++ had the same safety characteristics for at least a decade with dozens of linters and sanitizers on the market
C++ does not, and did not, have safety features like rust's. C++ cannot guarantee ownership semantics are correct. It cannot identify data-races at compile-time. Leaving dangling references in C++ is trivial.
For example:
std::vector<int> v = {1};
auto ref = &v[0];
v.push_back(10);
std::cout << *ref;
That code compiles in c++ even though 'ref' may be invalid because 'v.push_back' may have reallocated the underlying memory. I don't know of any compile-time tooling for c++ that reliably catches that bug. It's a clear memory safety issue, and it's basically the exact thing rust's ownership system saves you from.In short, I don't think you have a complete understanding of what rust's benefits are nor why it's innovative.
It's niche, been around for over a decade, still hasn't reached critical mass. Golang did. Numerous mainstream projects are in Go that drive entire industries now e.g. docker, k8s. Rust's biggest deployed code is the CSS renderer of a web browser with single digit market share. I'm not saying Golang is better on PL theoretic grounds (obviously it's worse), but it proves what corporate marketing can do, which we have known since the Sun era. Also Python was not that popular until the same corporation behind Golang decided to adopt it and hired Guido.
C++ compilers don't catch all the errors, but static analyzers do. Taking your code snippet as an example:
% cppcheck ./main.cxx
Checking main.cxx ...
main.cxx:8:16: error: Using pointer to local variable 'v' that may be invalid. [invalidContainer]
std::cout << *ref;
^
main.cxx:6:13: note: Address of variable taken here.
auto ref = &v [0];
^
main.cxx:7:2: note: After calling 'push_back', iterators or references to the container's data may be invalid .
v.push_back (10);
^
main.cxx:5:19: note: Variable created here.
std::vector<int> v = {1};
^
There are proprietary static analyzers that go much deeper and catch less obvious bugs. Even without paying, you have ASan, TSan, UBSan, valgrind, afl, among others to do runtime analysis or fuzzing to catch memory errors and data races. Unfortunately these aren't used as much as they should be. Maybe those programmers have a point. In practice, memory management is not remotely the most difficult or bug-prone part of software. If you are using C++11 or later, the standard practice is to never do raw new and delete. std::move(std::unique_ptr...) has the same benefits as Rust borrowing without the borrow checker (although it is not rocket science to detect use-after-move with analyzers). Arc <-> shared_ptr. Sure, Rust's versions are better, but let's not get carried away, it's <10x better than C++ (with 1/10000x the ecosystem, jobs, books, libraries, people).Rust 1.0 happened in 2015; everything before that was incompatible with other releases, sometimes multiple times per day.
<source>:8:18: warning: dereferencing a dangling pointer [-Wlifetime]
std::cout << *ref; // D: error – because pset(p) contains {invalid}
^~~~
<source>:7:5: note: modified here
v.push_back(10);
^
https://godbolt.org/z/wbRghNRust biggest contribution has been to force other languages to up their game in memory security.
Genuine question.
The borrow checker in rust has no runtime overhead (whereas go's race flag is runtime only, and has so much overhead you can't turn it on in prod). In addition, the borrow checker catches data races, but it also catches more subtle ownership issues too!
For one example of an ownership issue which isn't also a concurrent data-race, let's look at something in go that the -race flag doesn't catch, but that the borrowchecker in rust does:
// go
b := []byte("foo bar")
foo := b[0:3]
fmt.Println(string(foo))
b = b[:0]
b = append(b, []byte("not foo")...)
fmt.Println(string(foo))
// prints "not"
// This is because 'foo' is actually "borrowing"
// bytes from b, but go doesn't have any way to indicate
// that said "borrow" is active, so writing to it might be a bug.
That code is here: https://play.golang.org/p/0EK2OzeUG0vNow let's try to write code that has that same bug in rust:
let mut b = "foo bar".as_bytes().to_vec();
let foo = &b[0..3];
println!("{}", String::from_utf8_lossy(foo));
b.truncate(0);
b.append(&mut "not foo".as_bytes().to_vec());
println!("{}", String::from_utf8_lossy(foo));
That code is here: https://play.rust-lang.org/?version=stable&mode=debug&editio...If you try to compile it, you'll see that it doesn't compile with the following error: "error[E0502]: cannot borrow `b` as mutable because it is also borrowed as immutable"
In the rust code, the borrow checker keeps track of the fact that "foo" is currently a reference into "b", and because of that, it knows that it shouldn't let the programmer modify the memory out from under "foo".
This is just one of many examples I could give, but hopefully it provides some explanation of how the borrow checker is more flexible than just race detection.
Basically, there's a lot of cases in programming when it's useful to have a function which takes ownership of an object and turns it into something else, or borrows an object temporarily but then gives back ownership. In languages like go, the fact that "close(channel)" means that the channel can't be written to again is a runtime guarantee. In rust, it can be expressed to the compiler that 'channel.close()' means that channel is gone and using it again is a compile time error. Ownership ends up being a really common concern, and the borrow checker handles it nicely.
Rust doesn't need an 'assist' but articles like this will certainly damage the, otherwise sterling, reputation of it's community.