We’re big fans of safety-focused languages at FP Complete. As our previous blog post comparing Rust and Haskell made clear, we think both of these are great languages. A recurring theme we have for both internal and client projects is: which language should we use? The general guidelines in that blog post apply:
A large part of the higher safety of Haskell is its expressive type system. You can simply state more invariants, in general, in Haskell than in Rust.
However, I’d like to point out a situation where Rust’s safety guarantees are in fact stronger than Haskell’s, and how the compiler naturally pushes us towards the correct, safe solution.
References in Haskell
My general recommendation about mutable variables when giving Haskell training is:
If you follow that advice, the bug below won’t occur, or at least won’t occur implicitly. That said, the code below is entirely valid Haskell, so I don’t feel that cheeky pointing it out.
Question: Will the following code throw an exception, or will it exit successfully?
This code creates a reference. Then, bracketed by some function
So will the code throw an exception? The right answer is really “it depends.” We’ve invoked a function called
One valid definition for
And with this definition, our code exits successfully. Hurrah!
However—and you may have seen this coming—it’s fairly easy to come up with an implementation of
We’ve now spawned 1,000 threads running concurrently, all trying to write to the same reference. Assuming one version of how these threads get scheduled, all 1,000 threads first write their values to the shared reference. Then, they all take a nap for 1ms. Finally, they all read the value, and 999 of them are disappointed with the result.
The right answer to this in Haskell is what I said above: avoid mutable references, and prefer STM. If you use STM, you’ll need to explicitly bracket your mutable actions with an
Click below to learn more about a unique offer
How about Rust?
Let’s write an equivalent
Ignoring silly cases like
Firstly, let’s try out the identity case for Rust. Writing higher order functions is a bit more verbose than in Haskell, but not too bad:
Pro tip, especially to those who read my Rust crash course lesson 5: you can use
If you squint hard enough, you’ll probably agree that this is basically the same as the above.
Alright, that’s all well and good, but how about throwing in a curve ball with multiple threads? Let’s really test out that Rust “fearless concurrency” concept:
This code spawns 1,000 threads, each running the same
Obviously this code must fail at runtime, right? It has the same kind of logic bug as the Haskell code. However, the Rust code doesn’t even get that far. Instead, we get a compile time error:
Oh, right. We can’t send data between threads unless we have a
But now we get a new error:
Darn, now we’ve got two problems: one in the
Ugh, this is getting tedious. Fine, let’s try to solve the problem in
Now, we’ve finally written a valid
The closure captures a mutable reference to a variable. But mutable references cannot be cloned! Because, if we did that, we’d end up with a data race. (Kind of like that original bug in the Haskell program, right?) We’ll also run into a lifetime problem, but the compiler hasn’t even identified that here since it’s so hung up on the lack of a
Well, we know how to solve this kind of situatuion in Rust: use a reference counted wrapper. Let’s go ahead and throw that at the problem:
And, once again, Rust doesn’t like this:
Switching over to an
But the compiler isn’t happy with this either!
We’ve gone ahead and locked a mutex, ensuring that no other thread has the possibility of modifying our variable while we’re working with it. This code will compile successfully (finally!), and will complete successfully at runtime.
While Haskell generally has better safety guarantees than Rust, there are some cases where the explicit tracking of lifetimes and thread safety—versus Haskell’s garbage collection and explicit-mutable-types—produces a safer result. Specifically, in our case, the following features of Rust forced us into using the right solution:
The following features also play in, but overlap strongly with Haskell:
Breaking the Rust code
I don’t want to imply that it’s impossible to make a logic error in the Rust code. What I am saying is that Rust naturally pushed us towards the correct solution here. However, it’s possible for us to reduce the scope of the mutex lock and end up in the same situation as the Haskell code:
However, this code is more blatant about its lack of thread safety, just like the Haskell STM implementation is:
While both the Haskell and Rust versions presented will fail at runtime, it’s fair to claim that the language and libraries did all they possibly could to help the programmer avoid the problem. In the Rust case, we explicitly delimited the scope and locked the mutex twice. In the Haskell code, we explicitly elected to perform two different atomic transactions.
When dealing with concurrent code—or really any code—it’s impossible to create a language that will prevent the possibility of all logic errors. Instead, our hope is that we avoid most logic errors with the help of our tooling. And I believe both Haskell and Rust assist with this significantly.
Posted by Michael Snoyman - 17 January, 2019