Why I’m very close to giving up on Rust after just a week
I read “the book” over summer, slowly, making copious notes along the way. Last week I began to write something using it, and after a week I think I’ve seen enough. I’ll probably give up my sunk costs, because I value my time and sanity much more.
I expected the borrow checking to be “the problem”, but I understood those concepts easily. You flow references around, or clone them. I’ve written low-latency trading systems in C# and multithreading is familiar to me, so the advanced smart pointers were pretty easy to wrap my head around.
None of that is the problem.
The book touches on generics, it seemed fine in print, but as soon as I was using a web framework, which is good way to front-load the developer experience of what the thick end is like, I found myself in the deplorable type system morass that many TypeScript developers create for themselves and their poor colleagues.
I lack the words to explain it, but Rust is actually very similar to TypeScript in that its type system is “so powerful” that it ends up dominating the way code is written.
But it’s worse than TypeScript because it has a half-baked async system as well as lifetime bounds, stacks of little wrapping smart pointer types, and just clever clever features which become unfathomable. You can’t reverse engineer your way out of problems because of layers of convoluted and stacked type inferencing, and even things called associated types, each with their own bounds and constraints.
But it’s the inference upon inference upon inference upon inference that the compiler does, that a human cannot do, or at least this human cannot do without literal headaches and sadness. It’s fine when it’s working, or you wrote the code yourself, but as soon as you go off-piste from the framework’s documentation, you’re fucked.
It’s a Rubiks cube of a language that’s not worth the pain for most applications of it.
I’ll try Go, again. I read a book but didn’t have a project to use it on. I loved the idea that Go has barely any features, has barely changed or added features, has one way to do things, and is very fast and almost as fast as Rust in some tests. Plus, decent companies use it — the companies using a language is an important thing.
After more than two decades coding, I realise that simplicity and productivity are the thing, which foster deep joy. Rust is absolutely not simple and not worth the productivity trade off for anything other than replacing worse languages.
I read a few reviews of people who’d put years into it and I was looking for slam dunk “it’s amazing” and I saw mixed feelings. That’s not good. I assume so many people apparently love Rust because of where they’ve emigrated from, and possibly because of their own bad practices in other languages.
Rust leans on some types for maybe returning a result, which are just patterns you can use in any language.
You know, using a compiled, type safe language with basic generics while never creating a null can get you very far. In my experience, most problems come from the single mistake of returning null, or instantiating structs with nulls. The second bunch of headaches come from not failing soon enough, such that you end up with a trace that’s far from the source of the problem.
Really, these two things are most of the pain of coding.
Edit
Since this post got some attention and pushback, bellow is an example of the problem. Remember I’ve read the book, hand written notes from every page, then typed those notes up into my GitHub notes bit. I actually did the coding tutorials in the book, too.
But also, I’ve been coding in strongly-typed languages since 1997. I have been coding in general for almost 40 years and I have an IQ of 150.
I spent around 2 hours just staring at the code below, trying to wrap my head around it and what it meant for me as a user of the framework, trying not to lean too much on AI for help, either, as I need to be able to train myself to almost instantly understand this sort of thing and its ramifications.
I can logically break it down, but I am unwilling to work in a language which allows people to write Gordian Knots, or perhaps on a project where Gordian Knots will be likely, such as a whole application. It’s the same killer for TypeScript code. I’m certain Rust is great for small components.
It was while I was coding a method to implement something like middleware using a simpler way than this, when I changed something, I don’t remember what now, but it seemed to alter the return type, and there was no way of knowing from the compile error, or my code, or the source code of the framework, how to solve the riddle of what needed to be returned such that it satisfied all the inferred types and thus implemented the trait it needed.
The answer was likely a long way from the code I was in. It’s like troubleshooting an exception that has been raised a long way from the source of the problem, but worse, because it’s distant type-inference gymnastics.
Programmers have a tendency to write knots that kill the joy for all their colleagues. It’s an accretive process; they multiply-up the knot slowly, keeping it all balances in their short-term memory, so they understand it and don’t see the knot, and if they do, they have sunk-cost fallacy. In my experience there seems to be a principle of conservation of complexity in code. Extreme complexity is pain.
Complexity can be concentrated into an area, it is often a choice, a trade-off, but happens often by accident, as above. Oops, I’ve riddled a knot. You can get the same logic done, albeit with more code, in a way that doesn’t get painful. The complexity is spread such that when zoomed out, it is focused in the entire codebase, but when zoomed in, each piece is digestible and pain-free.
That’s my theory, and I think it is also the theory that the creators of Golang had, and why they believed an ultra simple language was the best way to scale software in a large, fast-growing company like Google.
Here’s an example. I don’t know what this means for me as a user of the Actix framework. I can’t figure out what it means for me, what I need to do, just from code. My subconscious feels like it is trying to process a paradox that it can’t articulate to my conscious brain to even ask the right questions.
use std::future::{ready, Ready};
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error,
};
use futures_util::future::LocalBoxFuture;
// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
// next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
pub struct SayHi;
// Middleware factory is `Transform` trait
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for SayHi
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = SayHiMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(SayHiMiddleware { service }))
}
}
pub struct SayHiMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for SayHiMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
println!("Hi from start. You requested: {}", req.path());
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
println!("Hi from response");
Ok(res)
})
}
}
Trigger warning
I mentioned my IQ. Perhaps I’m an intellectual narcissist, though I had trouble spelling that.
There’s good reason for stating it in this context. It prevents people from dismissing the opinions of a stranger on the internet as simply not having the smarts for a thing, which is absolutely what people do. There is a term I hate, that hear a lot, “skill issue” which an ugly dismissive slur used by teenage boys, YouTubers. The very first comment was “what an idiot” and I went back and added that section.
It also shows people that this is a problem for high-IQ people, and so it makes Rust more exclusive, which I assume is undesirable. It shows that I have chosen not to continue, despite being able to get through this if I really wanted to.
I’m not willing to make this pain/speed trade-off in this context. It’s beyond the the fun/challenging sweet-spot, and there are other languages that are fast enough and widely used, for this use case.
I was interested in Rust for two reasons, I’ve never learnt C or C++, and it’s “the most-loved language”, so there’s curiosity and FOMO. But I don’t write stuff that needs this speed and safety, the opportunity doesn’t even arise (when do I suddenly need to write a driver?), so I do need to apply Rust to the reality of my work.