A big productivity gain/loss for me is the ecosystem. Rust has a very robust package manager with cargo and crates.io versus C. My IDE integrates with it very well and I can run tests on save via cargo watch. Overall it makes a very satisfying development loop.
I find the borrow checker to slow me down somewhat, but I feel like it saves me debugging time later. I’ve found that I really enjoy refactoring rust code versus Ruby.
I feel “proper” error handling in Rust is very time intensive and a velocity killer for me.
Ultimately I have much more confidence in my Rust programs. I did AOC for the first time in Rust last year and I was able to prototype fine. I felt it took a little longer sometimes but things like support of writing tests in the same file encourage me to use good practices that may have saved me time.
That being said, I don’t write games. Maybe it’s very different if that’s the case.
I feel “proper” error handling in Rust is very time intensive and a velocity killer for me.
I usually get a remarkably long way just by papering everything with anyhow::Result and using ? a lot haha. Once you’ve decided to make a library, thiserror::Error lets you derive most or all of the boilerplate for “proper” errors and is pretty great also.
anyhow and thiserror (sibling libraries, intended for applications and libraries resp.) are certainly the mainstream, de-facto standard choices. Personally, I favor the more integrated, full-featured or full-spectrum approach of snafu — OTOH, anyhow’s focus on ease of writing may be what someone who sees Rust error handling as burdensome needs. (Although snafu is essentially the same as having both anyhow and thiserror available, it encourages more the more effortful “thiserror” style over the “stringly-typed” “anyhow” style — while I share snafu’s preference, the “anyhow” style is surely more convenient to write.)
I feel “proper” error handling in Rust is very time intensive and a velocity killer for me.
I’ve said it before but in my decades of programming, Zig has my favorite error handling flow. It feels more right than just about anything else, for whatever reason.
Then again I liked Java’s checked exceptions back when people thought those were the end of programmer freedom as we knew it so what do I know.
I think there is so many dimensions to productivity it’s difficult for me personally to make a call on memory safety alone, but the author did a phenomenal job of discussing the options and they largely align with my own experiences.
For me my order of selection is: GC by default for anything I can use them on and I’d prefer that over the safety something like a borrow checker adds, then RC/borrow checker/MMM if I absolutely need it, but the selection of exactly which type specifically and which technology of that last group is more likely to be driven by other factors.
On the topic of the Rust’s borrow checker specifically, it has helped me write some applications that just worked first time in ways that I haven’t experienced in other languages, and anecdotally where less bugs have shown up. On the flip side I’ve struggled to TDD in Rust. I find that when I program in Rust I need to have a clear mental model of the types and interfaces ahead of time. Also, and maybe I’ve been unlucky with this one, but in my experience it’s much more likely that changing code over here could results in side-effect changes to code over there. Because of the difficulty to TDD I have less confidence in code I’ve written in Rust because I haven’t written failing tests before the functionality.
Other dimensions that I feel like play a large role in development velocity are API stability, solid standard libraries, fast and comprehensive tooling, and active communities on Stack Overflow, Discord, and elsewhere who are legitimately welcoming. These all go a long way to providing the primitives I need, helping me get to where I need to go whilst getting out of the way, getting me unstuck when I hit a wall, and make me feel like I’m part of a community where we’re all growing and not going it alone. The GC languages that have all these things are launch pads to developer productivity.
Maybe in the future we’ll get some languages with the other memory models that turn that around though.
This is overall a great post and I love the exploration taking place!
Garbage collection is probably the best approach for developer velocity. It completely decouples your goals from the constraints of memory management. You are free to solve your problem without tracking extra requirements, such as C++‘s single ownership or Rust’s borrow checking.
Once you reach a certain threshold of code size, garbage collection can intrude on your goals. Especially when the most popular GC’d languages have an interpreter and/or JIT. Launch times are crucial to development velocity, and most of these languages have significant overhead. MMM & friends have an initial learning curve, but if you have a deep understanding of the memory management requirements of your language and its ecosystem, you’ll be capable of writing code that solves your problem at a similar speed while getting an end product that is faster to run and debug.
If VisualVM is “the best approach for developer velocity”, I’m switching careers.
One user says, “Rust’s complexity regularly slows things down when you’re working on system design/architecture, and regularly makes things faster when you’re implementing pieces within a solid design (but if it’s not solid, it may just grind you to a total halt).”
How much of this can we attribute to the borrow checker? When I’m writing Rust, I feel like traits trip me up much more than ownership and borrow checking. The latter may have been a hurdle at the beginning, but once you start to think with the borrow checker imo it becomes easier. I’d be interested in cataloging the errors people run into to see if they’re mostly lifetime / borrow checker issues or trait issues.
From Using Rust at a startup: A cautionary tale: What really bites is when you need to change the type signature of a load-bearing interface and find yourself spending hours changing every place where the type is used only to see if your initial stab at something is feasible. And then redoing all of that work when you realize you need to change it again.
Rustaceans need to do a better job at showing newbies how to write slow, decoupled code. Abstract out your code with a trait and throw Box<dyn T> around. Clone everywhere. People write tightly coupled code in every language, but Rust gets a lot of complaints because the decoupling process can be a pain.
Pony is a great example of how a garbage collected language can reach further towards correctness than other languages. Pony is one of the only languages that literally cannot cause a run-time error. Like Erlang, it is incredibly resilient.
The author says that borrow checking “imposes extra constraints compared to other paradigms”, and that this can hurt prototyping and iteration. Pony has 6 pointer types with different aliasing restrictions. Here is where I think the author’s bucketing of languages becomes leaky: Pony has a garbage collector yet also maintains the pointer managing complexity of Rust / C++. It seems to me that the design of the memory model & concurrency model together dictate velocity more than either one alone does.
As you can see, software development can be much more expensive than power usage, so it makes sense to primarily optimize for development velocity.
The trade off expressed here is that old chestnut, “programmer time is more valuable than computer time”. It’s worth zooming out a bit and considering where that got us: endless software bloat in all aspects, be it size on disk, memory usage, and UX latency. The author is correct that from the narrow perspective of a profit-driven enterprise this tradeoff makes sense. Perhaps it would make less sense if electricity producers or consumers had to pay the full value of their negative externalities at $100/ton/CO₂, for example. Currently the voluntary tech industry price is about $1.29/ton/CO₂; contrast this with the mandatory compliance market which is at $77.52/ton/CO₂ for Europe: https://carboncredits.com/carbon-prices-today/
Also I think they’re just plain wrong. Their figure for electricity costs is $3.085 billion, and labor costs is $4.856 billion. Those numbers are not that far apart from each other. It’s certainly not the factor of 10x or 100x that usually makes you say “the cost of X is insignificant compared to Y”.
Add into the fact that Google runs its own data centers and so its costs per unit time aren’t just electricity, but also real estate, building maintenance, property taxes, etc? Yeah.
For games specifically, I’ve found that knowing how to make a game is a much bigger barrier to entry than how to write Rust or how to write F# or such. You can know each of those languages immaculately and still struggle with structuring, say, a roguelike vs a sidescroller vs a fighting game vs a visual novel. And if you know the patterns and concepts that work well in one of those genres then writing the game in Rust, Go and Zig will look more similar than different.
Both my kids periodically ask if I could write a video game and I always say something like “I can write the code, sure, but I have no idea how to make it fun.”
You know how to make it fun by playing your game. Easy way is to start with an established premise and fiddle around with it. Start with Pac-Man, now what could you change about i– oh, what happens if you add a powerup that lets you chew your way through walls? Or maybe looks around room you have a pet Pac-Dog that follows you and barks at ghosts at random to scare them away? Honestly a cat is probably scarier to ghosts than a dog is, but a cat would just wander around the playing field at random and probably sit on top of the walls looking smug most of the time. Would that play better than a loyal but easily distracted dog that tries to protect you? Let’s try it and find out I guess.
Without exciting to sound rude and with agreeing overall I think there is a lot of claims made here that only seem to be based on gut feelings
Despite the a section dedicated to concurrency, the author seems to misunderstand it in practice.
I suspect they vastly underestimate the amount of concurrency found in the wild.
A big productivity gain/loss for me is the ecosystem. Rust has a very robust package manager with cargo and crates.io versus C. My IDE integrates with it very well and I can run tests on save via cargo watch. Overall it makes a very satisfying development loop.
I find the borrow checker to slow me down somewhat, but I feel like it saves me debugging time later. I’ve found that I really enjoy refactoring rust code versus Ruby.
I feel “proper” error handling in Rust is very time intensive and a velocity killer for me.
Ultimately I have much more confidence in my Rust programs. I did AOC for the first time in Rust last year and I was able to prototype fine. I felt it took a little longer sometimes but things like support of writing tests in the same file encourage me to use good practices that may have saved me time.
That being said, I don’t write games. Maybe it’s very different if that’s the case.
I usually get a remarkably long way just by papering everything with
anyhow::Result
and using?
a lot haha. Once you’ve decided to make a library,thiserror::Error
lets you derive most or all of the boilerplate for “proper” errors and is pretty great also.anyhow
andthiserror
(sibling libraries, intended for applications and libraries resp.) are certainly the mainstream, de-facto standard choices. Personally, I favor the more integrated, full-featured or full-spectrum approach ofsnafu
— OTOH,anyhow
’s focus on ease of writing may be what someone who sees Rust error handling as burdensome needs. (Althoughsnafu
is essentially the same as having bothanyhow
andthiserror
available, it encourages more the more effortful “thiserror
” style over the “stringly-typed” “anyhow
” style — while I sharesnafu
’s preference, the “anyhow
” style is surely more convenient to write.)I’ve said it before but in my decades of programming, Zig has my favorite error handling flow. It feels more right than just about anything else, for whatever reason.
Then again I liked Java’s checked exceptions back when people thought those were the end of programmer freedom as we knew it so what do I know.
I think there is so many dimensions to productivity it’s difficult for me personally to make a call on memory safety alone, but the author did a phenomenal job of discussing the options and they largely align with my own experiences.
For me my order of selection is: GC by default for anything I can use them on and I’d prefer that over the safety something like a borrow checker adds, then RC/borrow checker/MMM if I absolutely need it, but the selection of exactly which type specifically and which technology of that last group is more likely to be driven by other factors.
On the topic of the Rust’s borrow checker specifically, it has helped me write some applications that just worked first time in ways that I haven’t experienced in other languages, and anecdotally where less bugs have shown up. On the flip side I’ve struggled to TDD in Rust. I find that when I program in Rust I need to have a clear mental model of the types and interfaces ahead of time. Also, and maybe I’ve been unlucky with this one, but in my experience it’s much more likely that changing code over here could results in side-effect changes to code over there. Because of the difficulty to TDD I have less confidence in code I’ve written in Rust because I haven’t written failing tests before the functionality.
Other dimensions that I feel like play a large role in development velocity are API stability, solid standard libraries, fast and comprehensive tooling, and active communities on Stack Overflow, Discord, and elsewhere who are legitimately welcoming. These all go a long way to providing the primitives I need, helping me get to where I need to go whilst getting out of the way, getting me unstuck when I hit a wall, and make me feel like I’m part of a community where we’re all growing and not going it alone. The GC languages that have all these things are launch pads to developer productivity.
Maybe in the future we’ll get some languages with the other memory models that turn that around though.
I’m looking at https://www.roc-lang.org/ and its notion of “platform”, allowing to chose/mix different memory models.
This is overall a great post and I love the exploration taking place!
Once you reach a certain threshold of code size, garbage collection can intrude on your goals. Especially when the most popular GC’d languages have an interpreter and/or JIT. Launch times are crucial to development velocity, and most of these languages have significant overhead. MMM & friends have an initial learning curve, but if you have a deep understanding of the memory management requirements of your language and its ecosystem, you’ll be capable of writing code that solves your problem at a similar speed while getting an end product that is faster to run and debug.
If VisualVM is “the best approach for developer velocity”, I’m switching careers.
How much of this can we attribute to the borrow checker? When I’m writing Rust, I feel like traits trip me up much more than ownership and borrow checking. The latter may have been a hurdle at the beginning, but once you start to think with the borrow checker imo it becomes easier. I’d be interested in cataloging the errors people run into to see if they’re mostly lifetime / borrow checker issues or trait issues.
Rustaceans need to do a better job at showing newbies how to write slow, decoupled code. Abstract out your code with a trait and throw
Box<dyn T>
around. Clone everywhere. People write tightly coupled code in every language, but Rust gets a lot of complaints because the decoupling process can be a pain.The author says that borrow checking “imposes extra constraints compared to other paradigms”, and that this can hurt prototyping and iteration. Pony has 6 pointer types with different aliasing restrictions. Here is where I think the author’s bucketing of languages becomes leaky: Pony has a garbage collector yet also maintains the pointer managing complexity of Rust / C++. It seems to me that the design of the memory model & concurrency model together dictate velocity more than either one alone does.
From the article:
The trade off expressed here is that old chestnut, “programmer time is more valuable than computer time”. It’s worth zooming out a bit and considering where that got us: endless software bloat in all aspects, be it size on disk, memory usage, and UX latency. The author is correct that from the narrow perspective of a profit-driven enterprise this tradeoff makes sense. Perhaps it would make less sense if electricity producers or consumers had to pay the full value of their negative externalities at $100/ton/CO₂, for example. Currently the voluntary tech industry price is about $1.29/ton/CO₂; contrast this with the mandatory compliance market which is at $77.52/ton/CO₂ for Europe: https://carboncredits.com/carbon-prices-today/
Also I think they’re just plain wrong. Their figure for electricity costs is $3.085 billion, and labor costs is $4.856 billion. Those numbers are not that far apart from each other. It’s certainly not the factor of 10x or 100x that usually makes you say “the cost of X is insignificant compared to Y”.
Add into the fact that Google runs its own data centers and so its costs per unit time aren’t just electricity, but also real estate, building maintenance, property taxes, etc? Yeah.
For games specifically, I’ve found that knowing how to make a game is a much bigger barrier to entry than how to write Rust or how to write F# or such. You can know each of those languages immaculately and still struggle with structuring, say, a roguelike vs a sidescroller vs a fighting game vs a visual novel. And if you know the patterns and concepts that work well in one of those genres then writing the game in Rust, Go and Zig will look more similar than different.
Both my kids periodically ask if I could write a video game and I always say something like “I can write the code, sure, but I have no idea how to make it fun.”
You know how to make it fun by playing your game. Easy way is to start with an established premise and fiddle around with it. Start with Pac-Man, now what could you change about i– oh, what happens if you add a powerup that lets you chew your way through walls? Or maybe looks around room you have a pet Pac-Dog that follows you and barks at ghosts at random to scare them away? Honestly a cat is probably scarier to ghosts than a dog is, but a cat would just wander around the playing field at random and probably sit on top of the walls looking smug most of the time. Would that play better than a loyal but easily distracted dog that tries to protect you? Let’s try it and find out I guess.
Tada, game design!