50 comments

[ 3.9 ms ] story [ 71.1 ms ] thread
I encourage every one to at least stop writing code in Python.
How do you encode the locking issue in the type system, it seems magical? Can you just never hold any locks when calling await, is it smart enough to know that this scheduler might move work between threads?
Though it's not the only benefit, I enjoy rust and fsharp's typesystems most when refactoring code. Fearless refactoring is the right expression here.
Most of these productivity gains are achievable in any Standard ML influenced type system.
The first save is from a failure of Rust's own making: async Rust. It's an awful footgun of a concept that is made vaguely bearable by the rest of the type system.
I find the Zig example to be shocking.

It's just so brittle. How can anyone think this is a good idea?

The typescript/javascript example is a little dishonest. Nothing will save you if you don't know how the runtime/environment/domain semantics work.
>The code will compile just fine. The Zig compiler will generate a new number for each unique 'error.*'

This is wild. I assume there's at least the tooling to catch this kind of errors right?

I agree with the premise of more sophisticated compilers give you a productivity boost. But compiling with cargo can be very slow, and having a strong workstation can be worth it.
> Assigning a value to 'window.location.href' doesn't immediately redirect you, like I thought it would.

That's not a "Typescript" or language issue, that's a DOM/browser API weirdness

Don't most of the benefits just come down to using a statically typed and thus compiled language? Be it Java, Go or C++; TypeScript is trickier, because it compiles to JavaScript and inherits some issues, but it's still fine.

I know that Rust provides some additional compile-time checks because of its stricter type system, but it doesn't come for free - it's harder to learn and arguably to read

No. Other languages don't prevent concurrency related bugs like the one in the article. Rust has reference aliasing rules and lifetimes and send and sync traits. These things do not exist in Java and others, so will not prevent such bugs
I had to explain this to my students today using XKCD #1987, that although Python is considered a universally "simple" and "easy to use" language, a lot of the complexity in that ecosystem is paid for on the backend.

With Rust, they frontload the complexity, so it's considered to be "hard to learn". But I've got to say, Rust's "complexities" have allowed me to build a taller software tower than I've ever been able to build before in any other language I've used professionally (C/C++/Java/Swift/Javascript/Python).

And that's the thing a lot of people don't get about Rust, because you can only really appreciate it once you've climbed the steep learning curve.

At this point I've gone through several risky and time-consuming (weeks) refactors of a substantial Rust codebase, and every time it's worked out I'm amazed it wasn't the kind of disaster I've experienced refactoring in other languages, where the refactor has to be abandoned because it got so hairy and everyone lost all hope and motivation.

They don't tell you about that kind of pain in the Python tutorial when they talk about how easy it is to not have to type curly braces and have dynamic types everywhere. And you don't really find that pleasure in Rust until you've built enough experience and code to have to do a substantial refactor. So I can understand why the Rust value proposition is dubious for people who are new to the language and programming in general.

[1] https://xkcd.com/1987

Code written below your line gets executed if you don't return early. More breaking news at 8.

Seriously, why would you think that assigning a value would stop your script from executing? Maybe the Typescript example is missing some context, but it seems like such a weird case to present as a "data race".

I always thought that the POSIX threads semantics that forces the thread acquiring the lock to be the same thread that releases it, is too strict and not needed. In certain use cases it forces you to redesign the code in more complicated ways.
Using sync locks in an async environment is a code smell, so the Rust code is bad right off the bat. Just stick with async locks and take the tiny performance hit unless you know exactly what you're doing.
I didn't need to hold the lock across the .await point and in that case the official docs recommend a regular lock.
Rust is one of a very few languages where I can make a total mess in my code by refactoring, have like 100+ compilation errors in multiple modules, think I’m never going to fix it, yet at the end of the day I eventually get everything to compile fine, I run the tests and… they all pass from the first go. All green. Hard to believe but it happened many times to me.
The Typescript example given is an obvious bug to me even before I knew how the window redirect thing works. It was an obvious control flow issue

And I find the event loop vrs concurrency via mutexes to be like an apples to oranges comparison. They both do some form of concurrency but not nearly in the same way

Again, Typescript is hardly considered a language. It is just a tool used to keep javascript under some control on large projects. Again, the comparison between rust and Typescript on a language level is not a great match.

As for fearless refactoring, don't get me started. I experienced this the first time I was porting a vanilla js backend to a typescript version. It was awesome. I won't say it works in much the same way as rust does but man, if you ever ported a rest api written in javascript to Typescript - you'd experience a similar effect.

The javascript browser bug has nothing to do with the language.

It’s just a logic bug.

E.g., the code doesn’t match their own English description of the logic: “If yes, redirect to the specific page. If not, go to the dashboard or onboarding page.”

The code is missing the “if not” (probably best expressed using an “else” clause following the if block).

On that graph, both curves should go monotonically down, just one faster than the other.
I would love to see a web app with an interface where I select two programming languages and it shows me a really clean snippet of code in language A and says "Look how terribly hard and unclean this is to achieve in language B". (and then you could reverse to see where B outshines A).

Languages are a collection of tradeoffs so I'm pretty sure you could find examples for every two languages in existence. It also makes these kinds of comparisons ~useless.

This post mirrors my experience 100%. The effect is even stronger when there are multiple people involved on a project, or when you're maintaining a project that you didn't start. There, the benefits of static typing and domain modelling with ADTs become essential. Take take GHC - a haskell project, not a rust one, but it's been under active development by many different people since 1990 and is still going strong. Is there any doubt that the guarantees provided by Haskell's type system were beneficial to making this possible? Not to say that it's strictly necessary – the linux kernel is an even more impressive project and it's in C – but something that is not necessary for success can still increase your odds of success.

You can ask any professional python programmer how much time they've spent trying to figure out the methods that are callable on the object returned by some pytorch function, and they will all tell you it's a challenge that occurs at least weekly. You can ask any C++ programmer how much time they've spent debugging segfaults. You can ask any java programmer how much time they've spent debugging null pointer exceptions. These are all common problems that waste an incredible amount of time, that simply do not occur to anywhere close to the same extent in Rust.

It's true that you can get some of these benefits by writing tests. But would tests have prevented the issue that OP mentioned in his post, where acquiring a mutex from one thread and releasing it from another is undefined? It's highly doubtful, unless you have some kind of intensive fuzz-testing infrastructure that everyone talks about and no one seems to actually have. And what is more time-efficient: setting up that infrastructure, running it, seeing that it detects undefined behavior at the point of the mutex being released, and realizing that it happened because the mutex was sent to a different thread? Or simply getting a compile error the moment you write the code that says "hey pal, mutex guards can't be moved to a different thread". Plus, everyone who's worked on a codebase with a lot of tests can tell you that you sometimes end up spending more time fixing tests than you do actually writing code. For whatever reason, I spend much less time fixing types than fixing tests.

There is a compounding benefit as well. When you can refactor easily (and unit tests often do not make refactoring much easier...), you can iterate on your code's architecture until you find one that meshes naturally with your domain. And when your requirements change and your domain evolves, you can refactor again. If refactoring is too expensive to attempt, your architecture will become more and more out-of-sync with your domain until your codebase is unmaintainable spaghetti. If you imagine a simple model where every new requirement either forces you into refactoring your code or spaghettifying your code, and assume that each instance of spaghettification induces a 1% dev speed slowdown, you can see that these refactors become basically essential. Because 100 new requirements in the future, the spaghetti coder will be operating at 36% the productivity of the counterfactual person who did all the refactors. Seen this way, it's clear that you have to do the refactors, and then a major component of productivity is whether you can do them quickly. An area where it's widely agreed rust excels at.

There are plenty of places we can look at Rust and find ourselves wanting more. But that doesn't mean we shouldn't be proud of what Rust has accomplished. It has finally brought many of the innovations of ML and Haskell to the masses, and innovated new type-system features on top of that, leading to a very productive and pleasantly-designed language.

(I also left this comment on reddit, and am copying it here.)

The graph has no units or scale. What is the project size at the crossover point? The article says "certain size" which is unclear.
(comment deleted)
Last year I ported a virtio-host network driver written in Rust, entirely changing its backend, interrupt mechanism and converting it from a library to a standalone process. This is a complex program that handles low-level memory mappings, VM interrupts, network sockets, multithreading, etc.

What’s remarkable is that (a) I have very little Rust experience overall (mostly a Python programmer), (b) very little virtio experience, and (c) essentially no experience working with any of the libraries involved. Yet, I pulled off the refactor inside of a week, because by the time the project actually compiled, it worked perfectly (with one minor Drop-related bug that was easily found and fixed).

This was aided by libraries that went out of their way to make sure you couldn’t hold them wrong, and it shows.

The Rust example that you point out might not trigger compile time warnings in other languages, but most OS mutexes (certainly the ones in Windows and Linux) will complain if you release them on a different thread than you acquired them.