
A deep read of Michael Snoyman's analogy between Haskell's immutable values, local mutation in `ST`, and the type boundary that keeps state from escaping.
ST monad, where mutation can happen inside a computation without becoming visible in the value returned by that computation. 1ST is a boundary that lets an implementation use local mutation while its public interface remains an ordinary pure function. That boundary, rather than the analogy itself, is the reason this post belongs in an FP reader's queue.[1, 2, 3, 4, 5][2, 3, 4, 5, 6][1, 2, 3, 4, 5]
[2, 2, 3, 4, 5]
[2, 3, 3, 4, 5]
[2, 3, 4, 4, 5]
[2, 3, 4, 5, 5]
[2, 3, 4, 5, 6]map (+1) is the obvious solution for this particular transformation. Snoyman's real question is what to do when an algorithm benefits from repeated in-place updates, but the surrounding program should still see immutable data.ST containsbase library describes ST as a state-thread monad that allows destructive updates. An ST s a computation runs in a state thread identified by s and returns a value of type a. The official API exposes the key escape hatch as:runST :: (forall s. ST s a) -> aforall s. The caller does not choose the state-thread identity. The computation must work for any fresh s, and the result type cannot contain that private identity. The documentation states the consequence directly: the internal state used by the computation is inaccessible to the rest of the program. 2STRef s anewSTRef, readSTRef, and writeSTRef all operate within the same s. A reference created in one state thread is therefore not a general mutable cell that can be handed to unrelated code. It belongs to that thread. 3counter :: Int
counter = runST $ do
ref <- newSTRef 0
writeSTRef ref 1
readSTRef refdo block are operationally real. The reference is written, then read. What disappears at the boundary is the reference and its state-thread identity. counter is just an Int.runST prevents the private state from being part of the returned result. The implementation gets a narrow mutable workspace; callers get a value with no handle into that workspace.ST does not turn arbitrary mutation into purity. It isolates mutation so that the observable behavior of the enclosing function can still be described as a value-producing computation. The distinction is also why ST should not be casually conflated with IO: the same module documents stToIO separately, with RealWorld marking a state supplied by IO rather than one of the private threads created by runST. 2ST. Snoyman suggests that the physical world might be analogous to one large ST action: change is real inside the process, while a higher-level view could treat the whole process as one contained transformation. He presents that as an analogy, not as a rigorous Kabbalistic model, and explicitly warns readers not to build theology on the Haskell comparison. 1ST, updates happen in sequence. A mutable reference can change. An array can be updated. The computation can use an imperative-looking algorithm. At the outer boundary, the only thing returned is the result, and the private state cannot escape through its type.runSTSTRef s a, but runST requires a result that works for every s while returning an a that does not mention the private thread. There is no outside name for the particular s allocated by that invocation. The reference cannot be smuggled out as a usable value.runST have separate internal states. The s parameter is not a global label. It is a scoped type-level identity. The official documentation describes it as keeping internal states from different runST invocations separate from each other and from stToIO. 2ST is a particularly clean instance because the scope is reflected in the type.ST, freeze the result, and return the immutable structure. 1 The exact choice of vector or array library is a separate engineering decision; the boundary principle is the part that generalizes.runST region is easier to reason about than mutable state spread across a module.ST removes the need to understand evaluation and space behavior. Even the Data.STRef documentation warns that the lazy modifySTRef can accumulate thunks and cause a space leak, recommending the strict modifySTRef' when that behavior is unwanted. 3ST, and it does not pretend to be. It starts with a performance problem, gives enough Haskell background to make the analogy accessible, then uses a personal religious connection to ask whether "change" has to mean overwriting an earlier state. The result is an essay whose technical value sits in the transition between levels of description.runST gives that idea a compact form: do stateful work in a private thread, return the value, and leave the thread behind.snoyman.com
hackage.haskell.org
hackage.haskell.org
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.