Clojure has a library called Core.Async that it allows you to model your system as a set of independent processes that pass messages to each other through channels. Each process is responsible for running itself, managing its own state, etc. But how do you manage state in a language where everything is immutable? Well there are many ways to do that using Atoms, Refs, and Agents. But those constructs are mostly about sharing mutable state between processes, and in many cases end up being more complex than you would need to implement state machine that entirely encapsulated its own state transitions. Fortunately there's another way.
The go block is the basis of each of the independent processes. These blocks define a unit of asynchronous work. The go block is generally implemented in a loop/recur fashion where a message is received from a channel, processed, and then the recur is called to loop back and wait for another message to be received. This looping gives the go block an opportunity to transform itself into a new state after each message is processed.
A Simple Example
A simple state machine might only have a couple of states and transform from one to the next.
Below you can see a go block that implements those three states: starting, playing, and done. It does so by starting in the ":starting" state and then transforming to ":playing" state when it recurs in the loop. The ":playing" state is run over and over until "some-condition" is met that makes it go to ":done".
Using Data to Determine State Transitions
A slightly more complex state machine might have multiple states to step through to process a series of messages. Think of an ATM as an example (a really simple ATM that is).
The ATM has multiple user interactions and must communicate with other services to actually authorize the transaction. When the ATM is done with a given user, it goes back into a waiting state until someone swipes their card again.
Instead of passing an explicit state through the loop, our implementation passes data around. This accumulator pattern allows for the state machine to collect data at each step and then to pass that data on to later states. The data itself is used to determine what state the state machine is in. If there's card information, but then the process expects a pin, if there's a pin it expects an amount, etc. Once it's accumulated the data it can process the transaction. It then transitions back to the waiting state by setting the transaction data back to empty so that the next iteration puts it back in the waiting state. Each of those states is a fairly simple process, but you can easily layer on more rules to implement more complex state transitions.
Sets of independent processes managing their own state, doing their own work, communicating by sending messages to other independent processes. Welcome to the world of simple asynchronous programs!