I think he should use a higher level framework, or wait a few years for them to mature, and use a garbage collected language until then.
I think he should use a higher level framework, or wait a few years for them to mature, and use a garbage collected language until then.
Pointing out weaknesses and things that might be done better is what helps something mature, not just praising it.
I don't see how it is [unduly] inefficient or uncomfortable for a language at Rust's level to ensure that the programmer actually thinks through the execution of the code they are writing. If one doesn't want to think about such things - and there's absolutely nothing wrong with that! - there are plenty of higher level languages that can do that legwork for them with more than good enough performance.
To me it feels a bit like pointing out that the process of baking a cake from scratch isn't very ergonomic, what with all the careful choosing and measuring of ingredients and combining them in the right order using different techniques (whisking, folding, creaming, etc). That is simply what goes into creating baked goods. If that process doesn't work for you, you can buy cake mix instead and save yourself quite a bit of time and energy - but that doesn't necessarily mean there's anything to be done better about the process of making a cake from scratch.
And to use your cake analogy, the fact that a process is complex or laborious is orthogonal to the degree to which it is ergonomic. You could imagine baking a cake in a well organized kitchen with well designed, comfortable tools, or you could imagine having to stand on your toes and reach to the back of a slightly-too-high cupboard every time you need to fetch an ingredient, and having to use a mixer with way too many settings you can never remember, and none that does exactly what you want. I think this is a better analogy for ergonomics in programming languages.
While it may be a better analogy, it doesn't really reflect the way the term is used. In my (admittedly anecdotal) experience, most people's complaints about ergonomics are more of expecting difficult things to be easier than they inherently are than of the actual quality of the tools they are presented with. People think they are complaining about a mixer with too many settings, when in reality what they are complaining about is the fact that there are several variables that go into mixing batter and dough. They buy a mixer that's targeted at a baker or chef who wants complete control over how their batter comes out or who even needs a mixer versatile enough to also mill grain or roll pasta, then are predictably lost because it's not as immediately intuitive to use as a simple hand mixer. I don't think that makes the mixer not ergonomic, I think it just makes it not the right tool for that particular person.
Sometimes complexity is a necessary consequence of the domain, and sometimes it's simply the result of poor design.
I believe the point the author is making is that they added a feature for ergonomics' sake that ended up not being ergonomic; async-style programming usually coincides with first-class functions and closures, and those are painful in Rust.
Are they, though?
The example the author gives in the article tries to write a multithreaded program as if it is a singlethreaded one. Of course that is going to be painful, whether you're trying to use futures/promises or you're using callbacks. If you want to use multiple threads safely (i.e. at all) you need to use the right structures for the job - smart pointers, mutexes, etc. This is independent of language really, even dynamically typed languages like Ruby still have mutexes for when you want to use multiple threads (though they do take control away from the programmer in the form of things like global interpreter locks).
If you don't actually want to deal with the complexities of multithreading, then don't use it. For example there are a good number of languages/runtimes that only expose a single-threaded event loop to the programmer (even though they may use thread pools in the background). But I don't think "ergonomic" should necessarily mean "abstract away every single detail" or "things should Just Work even though one is straight-up not approaching the problem correctly".
Note: I don’t really disagree with your view, but it seems that view is in the minority.
But now that state data that previously lived on a nice last-in-first-out stack has a different lifetime. Since Rust fastidiously encodes lifetimes in the type system, using async can result in having more complicated types to talk about.
To me it's just the price that must be paid to use a language that doesn't constantly spill garbage everywhere, necessitating collection.
The compiler complained: “error[E0308]: mismatched types”
Of course, if speed isn’t a concern for you, then please carry on.
Therr are many tasks where what you suggests is just too slow and unpredictable.
Don't get me wrong, I think JVM is great, it's just not systems level programming.
And Google's ads backend are hardly the definition of server. Their ads front-end for example always used to be in Java. Not sure what it is these days
In this specific case, only the spawned thread is mutating the Vec, but the Rust compiler is usually conservative, so it marked this as a bug.
The actual bug is one or both of the following:
1. One of the threads could cause the underlying Vec to reallocate/resize while other threads are accessing it.
2. One of the threads could drop (free) the Vec while other threads are using it.
In Rust, only one thread can “own” a data structure. This is enforced through the Send trait (edit: this is probably wrong, will defer to a Rust expert here).
In addition, you cannot share a mutable reference (pointer) to the same data across threads without synchronization. This is enforced through the Sync trait.
There are two common solutions here:
1. Clone the Vec and pass that to the thread. In other words, each thread gets its own copy of the data.
2. Wrap the Vec in a Mutex and a Arc - your type becomes a Arc<Mutex<Vec<String>>>. You can then clone() the data and pass it to the new thread. Under the hood, this maps to an atomic increment instead of a deep clone of the underlying data like in (1).
The Mutex implements the Sync trait, which allows multiple threads to mutate the data. The Arc (atomic ref count) allows the compiler to guarantee that the Vec is dropped (freed) exactly once.
It is just one thread that’s actually appending to the Vec, but it’s still a multi-threaded example.
struct Database {
data: Vec<i32>
}
impl Database {
fn store(&mut self, data: i32) {
self.data.push(data);
}
}
fn main() {
let mut db = Database { data: vec![] };
do_work_and_then(|meaning_of_life| {
println!("oh man, I found it: {}", meaning_of_life);
db.store(meaning_of_life);
});
// I'd read from `db` here if I really were making a web server.
// But that's beside the point, so I'm not going to.
// (also `db` would have to be wrapped in an `Arc<Mutex<T>>`)
thread::sleep_ms(2000);
}
No threads are spawned. fn do_work_and_then(func: fn(i32)) {
thread::spawn(move || {
// Figuring out the meaning of life...
thread::sleep_ms(1000); // gee, this takes time to do...
// ah, that's it!
let result: i32 = 42;
// let's call the `func` and tell it the good news...
func(result)
});
}
`you missed a snippet, a thread is spawned.
edit: formatting