Of course writing async code in Go is easier because of goroutines, but apart from that technically they are so different that it is a waste comparing the two.
Of course writing async code in Go is easier because of goroutines, but apart from that technically they are so different that it is a waste comparing the two.
Unfortunately, the borrow checker forces two extra constraints which often have little to do with the situation:
- No shared mutability. This is an unrealistic constraint; our world calls for shared mutability all the time. Any time a Go reference becomes an index in Rust, you're experiencing this mismatch.
- Single ownership (in the C++ and Rust sense). Most things can be phrased in terms of single ownership, but not all things should be, and it has a cost.
Rust offers you tools to enforce those constraints, but those constraints are often artificial complexity and shouldn't have been added to a situation to begin with. Same with async/await. They solve a self-imposed problem that e.g. goroutines show us don't need to exist in the first place.
We like these mechanisms because they make the program faster, but most of a program doesn't need to be fast. Only the hot path needs to be optimized, the rest of the program should be simpler, more flexible, more decoupled, and have less constraints.
(This is one of the reasons some domains should use Rc and RefCell more and our community should stop vilifying them: they help provide flexibility to balance the borrow checker's constraints.)
That said, this problem really only arises when one tries to use Rust in a domain it doesn't fit well. There are some domains which do fit the borrow checker's constraints really well, and they become benefits rather than sources of artificial complexity.
Isn't the ease with which a reference can be replaced with an index evidence that you usually don't actually need shared mutability?
> For example, in Rust, we often have to use an index or an ID where a regular reference would do in Go. We need to do extra refactoring of function signatures to pass collections down the call stack so we can then use that index/ID... and then find we can't modify a function signature because it's a trait override, and resort to tricky workarounds. In Go, we just use a reference. That choice is decoupled from memory concerns.
The problem is that you can't just dereference an index, you instead need all your callers' callers' signatures to take in the collection, causing a minor refactor shock wave in some cases. If that runs into an unchangeable signature (like a trait override, or a public API), it's pretty much game over and you're back to the drawing board.
If you're making CLI program or small server, this might not hurt that much. In larger programs, the extra refactoring from this constraint can be quite costly and disruptive.
Or is this a lifetimes issue, where the lifetime of the individual reference might be longer than the lifetime of the collection as a whole? In that case, it might be that the lifetime annotations aren't correct, or that the code is wrong in the first place.
I get the idea that you can't change public APIs and function signatures, but that's true in pretty much every language.
Hard disagree.
It is impossible to safely use shared mutable state b/w parallel processes.
See how DB isolation levels work - serializable is the only safe way to go if your processes do anything conditional on the current state. To parallelize, you have to partition the DB which is kind of a cheap way to split one physical DB into separate logical DBs.
So even DBs - the largest shared mutable states we have - are also not really shared mutable states. They need to be broken down into unshared mutable states to be useful.
Shared mutable state is a smell both at the code level and system level.
Also, you can still apply mutability restrictions at the region/thread/process level. You don't need to apply it to every single object, which would lead to the drawbacks discussed above. If you want to learn more about it, take a look at what Pony is doing.
[1]: https://corecursive.com/024-software-as-a-reflection-of-valu...
The problem is that so much of the time when you are building something you don't really have a clear idea of what you are doing, but you build and understanding as you experiment and iterate.
I do writing professionally now and have much the same experience. It is hard to plan exactly what you will write in detail. So much of the greatest ideas materialize as you write. Both writing and coding is IMHO a thinking process.
On the other hand I fully accept that us developers are all different in how our brains work. But I have seen when working with people much smarter than me how much they get stuff wrong and waste time by trying to excessively plan before fully understanding the problem. Stuff I notice I solve easily by taking an experimental and iterative approach.
One small concession: I think JavaScript is awful and Ruby projects tend to end up as a mess. I am mostly a Julia, Lua and Go fan. So I kind of learn towards languages which are a bit in between dynamic and static.
I have worked on one of the largest Ruby monoliths around and I love Ruby, but I also appreciate that Rust can cut through some class of problems that you can't with Ruby (similarly it is much easier to bend Ruby to your will than Rust).