This is definetly something the article should have drilled down on. Why Rust? I'm sure everyone's tired of hearing why rust is an excellent alternative to c/c++ for new projects, but as an alternative against Python it gets muddier. Rust has a clear advantage in performance and memory footprint and a much better multithreading story, but those are things that aren't high priorities for 95% of web development.
That basically leaves you with Rust's type system. Rust's type system is pretty great, and if we pretend we can't hear the Haskell developers it's one of the best type systems out there. That might seem to get in the way of quick prototyping, but on the other hand it would mesh really well with a framework like Django. One of the great things of Django was that you define your data schema, and Django takes care of both the database and a passable admin area. I'm sure you could greatly expand on that principle, with data types driving a lot of behavior and conveniences that the framework just handles for you.
Maybe a bit like .NET, but without the enterprisy coat of paint and without putting dependency injection everywhere.
The answer for why folks are so inclined towards doing high-level tasks in Rust... is the type system. Its sensibilities are in a sweet spot that makes it very easy to pull off huge refactors. It was also a lot of people's first introduction to algebraic data types being used in nearly all error handling (its usage of `Result<T, E> where E implements Error` and lack of nulls or exceptions). It makes a lot of progress towards the goal of "make invalid states unrepresentable", which could be really useful for web apps.
Do they really have better economics? (EDIT: ergonomics!)
Last time I looked into OCaml, I struggled to find a way to interact with the database in a type-safe way, I never figured out which standard library I was meant to install, I was being encouraged to use 2-3 different project management tools (Dune + Esy + OPAM), and I gave up on writing tests. But at least there's a garbage collector!
I realise these are all problems that I'll wrap my head around over time, and eventually they'll seem completely trivial to me, but the introductory documentation on getting started as a professional (and not as a first-year student doing a French-language compsci degree, which is what most of the documentation assumes), is pretty dire.
Meanwhile, much as I'm sure I overuse `.clone()` and `[A]Rc<_>`, the ownership model of Rust is deeply useful. It's something I often find myself missing in Javascript - not necessarily because I want to produce the most performant code, but because it's useful to understand the lifetimes of the objects I'm keeping around. Am I accidentally storing a reference to something in a closure that I forgot about? When this object gets deleted, have I checked that it's the last possible reference to this object? Etc.
Like, I don't think everyone needs to learn Rust. It's a great language, but there's lots of other great languages out there, depending on your personal and business contexts. But I think this idea that Rust can and should only be a low-level language feels absurd to me. It is a fairly ergonomic language with a fantastic ecosystem, a powerful type system, and an ownership model that will be useful even if you do try and opt partly out of it with GC-like wrapper types.
Thanks for pointing the typo out! I've fixed the comment.
In fairness, I think a lot of this comes down to familiarity. I'm fairly familiar with `.clone()` and Arcs at this point, so they don't really change much in terms of ergonomics. Usually their usage is fairly obvious, and quite often my editor literally does magically type the `.clone()` calls through LSP fix commands. It's the same as, say, OCaml's insistence that recursion is better for complex loops than, say, `while` — it's not necessarily wrong, but if you're unfamiliar with the idea, it's going to feel weird.
More important to me is the ergonomics of the broader ecosystem, and that's something that Rust has done well, that other languages just don't seem to be interested in at all. Things like integrating testing into the standard workflow; working hard on getting the stdlib in good condition; having an excellent ecosystem of well-documented, usable libraries; or designing error messages and lints with a focus on getting people to understand how the language works and not just what they've done wrong this time. I've really missed that stuff whenever I've tried MLs. You mention, for example, REPLs, but a unit test is basically a saved REPL session that you can repeat every time you make a change.
I'm not trying to convince anyone that Rust is the best language in the world or something. I really like it, but I find it helpful to think in terms of object ownership even in non-Rust languages, and I can understand why for other people that sort of approach is unhelpful. But I would love to see other languages embrace its ergonomics more, or new languages created with that as their focus.
A unit test has nothing to do with a REPL, a proper REPL provides something similar to jupiter notebooks in feel, alongside debugging and hot code reload experience.
Maybe we have different understandings of REPLs. For me, a REPL is a tool that I can use to try out some portion of my code, see how it responds to various inputs, see how it handles certain cases, and explore the internals of it via debugging tools.
But... that's what I do in a unit test anyway. A unit test is essentially a REPL session that I've frozen in time, can replay whenever I want, can debug, can trigger a fresh run whenever changes occur (which isn't quite hot reloading, but often a darn sight more useful), and can keep track of and share with my team. Which means I'm not just able to explore things on my own, but I can see the explorations that other people have made with their code.
Typing in modern days is effectively moving the task of debugging to compile/ide annotations process instead of testing for correctness.
Albeit it makes the process easier compared to a really shittly written code without strict typing, but against good codebases, it takes the same amount of time.
If you think about correctness of a program, (i.e for any combination of given input , it changes a state in a determinstic way, including no state change for invalid input).
Strict typing is one way to accomplish this. The cpu does not give a shit about types. It cares about memory registers and locations. The unique code built into the compliers/transpilers is the thing that validates the correctness of the program in this case.
You can move that code into the testing suite without relying on the complier, and just do testing. Generally, given competent programming skills, this takes about the same amount of time as designing a well structured program - your tests are pretty much your design document for the thing itself.
This is a very anti-pragmatic way of looking at things in my opinion. The program is not a snapshot that exists in a vacuum. Most programs are going to grow over time and have things added and removed in various places by many people. They're going to have many interfaces and operations.
I think you're arguing that tests and types can both be used to check a particular case for correctness. Sure, this is true. However "moving type checking code into the test suite" means nothing—that would just be type checking.
When you make a code change, there's a difference in the kinds of feedback you get from your tests and your types. Tests usually cover business logic or stories—things that you want or don't want. Types ideally cover everything. They check every operation applied to a given piece of data. Of course types are rarely precise enough that they can catch every logic bug (e.g. strings with semantics not encoded in types, like email addresses).
This is just scratching the surface. You might just have to try out both to get a better sense of how they feel to work with.
So at one of my older jobs, we worked primarily with C code that ran on a mini linux box inside a plane, and interfaces with a sensor pacakge. We wanted to make sure that the software was 100% correct cause any errors would mean an aborted flight test
We basically ended up creating a tool+language spec that would let us define the mapping of input to output sequences. We wrote it in such a way where we sat with scientists and pretty much mapped all the possible cases that they could think of for valid inputs and what the code should produce.
Then this tool would basically write automated tests for us, in such a way as to not only test correct behavior, but also do combinations of inputs out of order, fuzz testing, and so on. We ended up making it also check memory state, to ensure that there was no memory leaks, and analyze the memory space for required data or data that should not be there.
In the end, whenever someone was developing anything for this software, they would basically just run the tests, and it would be very good at catching possible errors, mostly on the negative side (i.e for a fuzzed input, it would result in an output that should be an error).
We could have done the same thing with a typed language, but it would have to be very strict typing to the point of something like CoQ, and it would have taken us probably the same amount of time to write that.
Hey, that's awesome. Seems like a great case for verifying things that way! I'm thinking of different cases I think, where you've got something like a distributed system or a web application and it's just harder to fuzz. But I agree, that seems a lot more practical than formally verifying it when that's the kind of testing you need.
Types [1] can only reason about categories of values, not about values. Tests and types do have an overlap, but the best option is both. Especially when combined with fuzzing, types can exclude large number of cases, making it even more efficient at covering a huge range of code paths.
[1] yeah, there are type systems like lean, coq that can do both, but the proving process is just currently not realistic for everyday applications
1. refactoring are in my experience still much faster (and reliable doable) with rust compared to e.g. Js/Python even in presence of a lot of tests. This is a bit less of an point with Java/TS/C# etc. through I had some very bad (and also some good) experiences with refactoring in TS. And to be clear I don't just mean pure refactoring but any larger changes affecting many places in code which might be needed to idk. impl. some feature.
2. especially with Js,Py and similar there are way to many edge cases you can have to test for all of them. Sure most times this mainly matters when writing libraries but on larger projects does apply too. Stupid stuff like you expecting a `list[int]` and someone (externally i.e. outside of your tests) passes a `dict[int, int]` and that happens to work as you current impl. is only linearly iterating it as if it's a `Iterable[int]` but then you change the impl to require a `Sequence[int]` as you access it by index in some corner case and now you customer has really strange subtle bugs. Can't be caught by your tests as the problem is the customer but still breaks the customer which is always bad and can't happen with rust. Sure also won't happen if everyone uses type annotations and mypy correctly and strictly. Through you can't rely on your customer using mypy, but you can rely on your customer running compiler checks (as it's the only way to build the code). Also while mypy is much much much better then pylance it is still prone to issues as both `cast` and `ignore[..]` are things you sometimes need but which easily can hide bugs if the code changes (cast doesn't pin the "from" type and ignore is scoped by place not by what is wrong)
People who say copilot is useless.... I can only imagine they're in a dynamically typed language. Copilot + Rust makes boilerplate go fast. Strong typing is force multiplier for code gen.
I suspect that’s true. I never blindingly copy LLM generated code (that would be recklessly stupid), but I often only quick skim rust code generated this way, just to make sure the general task it is solving is what I asked for. If there is an unhandled edge case or or memory handling bug, rust will catch it.
It's also easier to implement a generate_code -> check_for_errors -> give_error_to_llm -> fix_code loop, because the errors that rust throws at you are most of the time really well thought out, and as succint as usefully possible. Comparing it with python, where you have to write custom parsing to trim it down and even then it's hit and miss on where the actual error lies, it's not even funny.
Rust does not skirt around the fundamentals. You can still handle errors however you want, you can still blast side effects all over the place, and you still have to uphold an arbitrary set of invariants specific to the task at hand. But the ecosystem as a whole is filled with things being done the right way because the language and its linters have always been quite helpful.
In any program there are errors you can handle and errors you can't, errors whose vary existence suggests something has gone horribly wrong. What else should you do in that situation but panic?
I see unwrap used all of the time on things that could be recoverable if people bothered to write the control flow. “Meh, let the program crash” is easier because unwrap is much less verbose than the match unpacking or carrying results to callers.
Rust has exceptions, they are just named Result and people just as frequently decide not to handle error results as not catch exceptions in my experience.
Typed exceptions are just as good as what rust offers IMO.
Interesting, great example. This kind of compile time safety can't be achieved without borrow checker, I guess? I'm now thinking hard if there's some type magic similar to the exhaustive checks for Typescript discriminated unions ("kind satisfies never" checks in switch statements).
I've been trying to come up with a similar pattern in TS many times but I think you cannot do this due to the lack of moves (bindings do alias, pass by reference).
But TS has so many dark magical patterns that I'm still hoping to be proven wrong.
Shared mutable state is a problem even in single threaded code (for example, modifying a collection you're iterating over)
> And does Rust have type unions and intersections, interfaces, and mapped and conditional types?
You can typically accomplish the same thing with enums/proc macros/traits of course—with the additional benefit that the type system is designed to be sound. Soundness is an explicit non-goal of TypeScript[1], so once you start layering on those kinds of overly-clever types, you soon reach a point where you're just lulling yourself into a false sense of security.
JS has concurrency (but not parallelism) despite being single-threaded, though.
If you ever put an await anywhere in your code, an arbitrary amount of random stuff might run between the time you await and the time the awaiting finishes.
Same applies to older mechanisms like callbacks and promises.
Race conditions are more rare there because as long as you aren't doing any IO, there is indeed no concurrency, so you can't e.g. get two threads trying to increment a single variable at the same time. They can still happen, though, especially if you accidentally do a partial change to an object, put it in an illegal state, and do IO before you finish that change.
> The vast majority of errors caused by shared mutable state are related to concurrency.
Concurrency, like async-await, you mean?
> TypeScript's type system contains features that Rust's does not have
Sure, and likewise Rust's type system contains features TypeScript's does not have—for example, just try expressing anything close to traits with TS's type-erased generics. Since the feature sets aren't the same, I suppose it's a matter of opinion which type system is preferable. But I know which one is more helpful for me for sure (especially considering the aforementioned soundness issues).
idk. many years ago through stuff like service workers, having shared memory through TypedArrays and similar.
But for the discussion more relevant is that shared mutability constraints do not only matter with threading, they matter with any form of concurrency (like JS async/await/promises and before that callbacks). And even without that you still have other single threaded and single tasked concurrency with the classic being changing a collection while iterating over it. So even without classical forms multi threaded concurrency still very helpful.
> And does Rust have type unions and intersections, interfaces, and mapped and conditional types?
Rusts type system is mainly nominal typed while TS is mainly structural types so it's a bit hard to compare. Like if you nitpick then you could maybe argue that based on TS type system having "features rust types system doesn't have" it's more powerful (but you also could argue the other way around it depends on how you count them). But that would be misleading as it ignores that not only are they two fundamentally different approaches to typing it also ignores that some features are implemented through other means outside of the type system.
Practically having used both I can say that while TS has some things in the typing which are a bit cumbersome to do in rust when it comes to helping me having correct code in context of changes, especially larger changes, and especially libraries Rust still yields better results in my experience. Naturally assuming you don't abuse the TS in either case.
(and to technically answer the question, type unions == yes but different, intersection == no but also make little sense in rust, interfaces == yes but different, mapped types ~= depends on the aspect in some yes and better then TS in other no but implicitly through derives so worse then TS, conditional type ~= again handled through different features depending on usage either associated types or feature gates)
For what I hack on I often run into issues with the FFI performance between go/c# <-> c/c++. I’d rather not write C or C++ and Rust is one of the few languages that allows me to mess around with obscure libs at native performance and yet slap a web interface in front of it. cxx/bindgen is stellar in how fast they allow you to wrap libraries. These cases is where I would want like the simplest most opinionated web framework (like gin in Go).
Have you had real success with cxx? I've been trying to FFI a C++ lib (that I have limited control over) and it's been a real PITA. At this point I'm thinking of just making a pure-c interface for what I want on the C++ side and using C bindgen or just straight "extern C" because C++ FFI seems so painful.
FFI can be a source of performance issues in Go but not in C# (at least not to the same extent), unless you go out of your way to fight the happy path approach.
Sometimes you do have to rethink what you marshal vs what you just manage manually, but that’s what pointers are for (that can be turned into ref Ts for single values and into Span<T>s for slices/arrays/etc). The idea is that performance ceiling of FFI in .NET is as cheap as direct calls.
> but as an alternative against Python it gets muddier
For me, a clear advantage of rust (and also Go) over Python (or Ruby, or PHP) is not having to deal with runtimes.
With Python, the server needs all sorts of setup. Exactly the right version and configuration of the runtime. Pip. Or Pipenv. Or Pyenv. Or Conda. Or... and so on. Then it needs the libraries installed. Some of which must be built natively; so just packaging them in some CI is often not even possible. These native builds need lib-something-dev packages.
Then with an update of the underlying server, suddenly nothing works anymore.
Having a single binary that I can plop onto almost every linux box and which "just runs" regardless of the exact version of Ubuntu, cargo, rust-toolchain on the server/hosting machine is invaluable.
I'm so done with maintaining the fragile card-house that my Ruby-on-rails apps require, or that my python/flask services need, that my nodejs insists upon: with rust this is no longer needed.
And yes, docker/containerization is a solution to it. And yes, I know about the options to package up a python app with runtime included. But all that adds extra complexity. It merely makes it easier sometimes. Not simpler. And it inevitably makes troubleshooting, upgrading, etc even harder in some future.
Between Rust and Python there are a huge distance full of options with managed compiled languages, with AOT, JIT, or even both AOT and JIT as standard toolchain feature.
> and if we pretend we can't hear the Haskell developers it's one of the best type systems out there
Eh, Rust's type system isn't one of the best out there. It's lacking higher kinded types, etc. It's abilities in type level programming are frustratingly limited as well.
So it's advanced aside from from Haskell, OCaml, Idris, Scala, etc. Compare to OCaml's effect system for an advanced type system feature.
I 100% agree with you (I really miss HKTs), but I don't think the GP was using "best" to mean "most fully-featured".
And with that in mind, I do agree with the GP. Scala's type system, for example, is full of warts (well, Scala 2; I still haven't tried Scala 3). Rust's is cleaner and has fewer gotchas. I very rarely have to look up how to express something in Rust's type system, but I remember when I last did Scala, I often ran into weird type-system related errors that I just didn't understand and had to dig into to figure out. Some of that, sure, is likely due to Scala's type system having more features, but some of that is because it's just more complex. And I would usually rate something of lower complexity as better than something with higher complexity.
One of the major things about Scala 2 vs Scala 3 is the removal of many of the type system warts, in particular the type hierarchy now forms a lattice (if I'm getting my terminology right) rather than being rather adhoc in various places.
Lots of other small annoyances like the whole tuple situation have also been fixed.
EDIT: Plus: intersection types, sane macros/inlining, etc.
Unfortunately (for me), some of our projects still have to cross-compile to 2.x, but that's irrelevant for greenfield.
I'd give it a whirl -- it's a great improvement over Scala 2.x.
OCaml's effects are fairly new, and the Rust maintainers are also looking into effect systems. Who knows, we might also get HKTs and dependent types too in the future.
That basically leaves you with Rust's type system. Rust's type system is pretty great, and if we pretend we can't hear the Haskell developers it's one of the best type systems out there. That might seem to get in the way of quick prototyping, but on the other hand it would mesh really well with a framework like Django. One of the great things of Django was that you define your data schema, and Django takes care of both the database and a passable admin area. I'm sure you could greatly expand on that principle, with data types driving a lot of behavior and conveniences that the framework just handles for you.
Maybe a bit like .NET, but without the enterprisy coat of paint and without putting dependency injection everywhere.