Go straight to STM

Everybody wants to know. Does Haskell have mutable state?

Yes! In fact, several different kinds – IORef, MVar, TVar – that have various tradeoffs depending on…

Ugh.


I’m not big on complaining about what things are named, but STM is a rough one for me. To explain why it’s called “software transactional memory”, we have to go back to the 90s.

The Tetris problem

There’s a saying that life is comparable to Tetris: While your mistakes are piling up, your successes disappear.

I think it beautifully describes a phenomenon in software: When people keep having to spend a great deal of time and code on a problem, it generates a great deal of discussion. Once there appears a solution that effectively mitigates the problem, there’s no need to devote so much talk to it anymore. Your successes disappear.

And I believe that’s what happened in 2005. STM solved a tremendous swath of multiprocessing problems in such a simple way that it all vanished in a puff of success. There aren’t enough flaws to make a popular conversation topic. Fantastic support for concurrency is, in my estimation, one of the greatest advantages of using Haskell to learn to code – and I’m not sure the programming population at large even knows that concurrent Haskell exists. From watching what most of us normally talk about, how would they ever find out?

Back in my “startup” days, I found out that it’s very difficult to switch back and forth between pitching a business and working on the product. The pitch has to be about everything that’s going right, but my everyday work focus is centered around what’s wrong. When I’m working on the product and somebody asks me about the business, all that’s on my mind is worries. And everyone can tell. You should never put me in front of investors.

Programmers all seem to hate their favorite languages, and I think it’s the Tetris problem. The good parts don’t require your attention.

One ref to rule them all

It was writing the Haskell Phrasebook that prompted our decision to ignore IORef and MVar and jump straight to STM. We wanted to quickly empower readers by presenting a small selection of the most generally useful APIs. When you’re stuggling through first steps in a language, you don’t need an overview of all the options; you need a guide to recommend a single good default.

This need for a minimal curriculum is especially pressing on the subject of state mutation, where the novelty budget is from the get-go already stressed by Haskell’s difference from other programming paradigms (more on that below). Three mutable state containers is two too many, so we picked STM as the one. Even for single-threaded applications, we’ll use a TVar; because then once you do need concurrency, you’re already prepared for it.

The great triumph of STM, and the reason it suffers the Tetris problem so badly, is that it does its job with very little contribution from the programmer. You are saved from many common sources of “race condition” and “deadlock” without even needing to know what those terms mean. TVars are simply state containers that work as expected in a multithreaded environment without weird surprises.

The potential downside of choosing STM is performance cost. To avoid it for this reason would fall squarely into the category of “premature optimization” for new language learners. Start with the thing that gives you a lot of safety with minimal effort.

Where the novelty budget is spent

In many languages, operations on state containers are built in using overloaded keywords. For example, the Python statement x = y can serve many ends, depending on context:

  1. Define a constant x whose value is y;
  2. Create a new state container x whose initial value is y;
  3. Replace the current value of the existing container x with the value y;
  4. Define a constant x whose value is obtained by reading from a state container y;
  5. Create a new state container x whose initial value is the value obtained from container y; or
  6. Replace the current value of the existing container x with the value obtained from container y.

In Haskell, x = y has only the first meaning, and the rest can be constructed using library functions like so:

  1. x <- atomically (newTVar y)
  2. atomically (writeTVar x y)
  3. x <- atomically (readTVar y)
  4. x <- atomically (newTVar =<< readTVar y)
  5. atomically (writeTVar x =<< readTVar y)

As Haskell users, we appreciate the clarity afforded by this extra verbiage. But we cannot allow our own affinities to distract us from the critical step of guiding transitioners over this undeniable hump of unfamiliarity.

State is for threads

The final reason for presenting TVar as the standard state container is that we rarely use mutable references in single-threaded programs. Even if we initially present state mutation using simple single-threaded demonstrations, the general aim of learning how to use state containers is to enable communication between threads. Since this is the job at which STM truly excels, programmers are better served by exposure to it sooner rather than later.


STM is the subject of chapter 10 in Sockets and Pipes, available for purchase on Leanpub.