StateT adds a threaded, pseudo-mutable state to an existing monadic computation. Instead of passing an updated model through every function explicitly, a script can read the current value with get, replace it with put, and still use the effects supplied by an underlying monad. This keeps stateful steps together while preserving an explicit execution boundary: running the computation requires an initial state and yields a result within the wrapped monad. section 7.2.2 section 7.2.3
Its shape is StateT state internalMonad result. The outer layer manages one state value; the inner monad remains available for its own capabilities. For example, StateT Board IO can update a game board and print messages. StateT is itself a monad, so ordinary do-notation sequences state reads, state updates, and lifted inner actions. section 7.2.3 section 7.2.3
Listing 7.6 Monad stack for a Minesweeper game
import Control.Monad.State (StateT) data CellType #1 = Bomb | Num Int | Empty data Cell #2 = Hidden CellType | Revealed CellType type Board = Map (X, Y) Cell #3 type BoardMonad a = StateT Board IO a #4
A board type alias makes the division of responsibilities visible: Board is the threaded state, while IO supplies console interaction. section 7.2.3
The key distinction from State is not how state is accessed—both use get and put—but what can happen alongside it. A State computation is pure state alone. StateT wraps another monad, so an action from that lower layer must be adapted into the StateT layer with lift. In a board-revealing action, get obtains the board, put stores a changed board, and lift (putStrLn ...) performs console output without abandoning the stateful script. section 7.2.2 section 7.2.3
Reveal a Minesweeper cell
A reveal operation first obtains the Board with get, checks the requested coordinates, and updates a hidden cell with put when appropriate. It uses lifted putStrLn calls to report missing, already revealed, bomb, or successful cases. The state transition stays local to the operation, while observable IO is clearly marked as belonging to the underlying layer. section 7.2.3
Listing 7.7 Lifting of IO actions
import Control.Monad.State (get, put, lift)
revealCell :: (X, Y) -> BoardMonad ()
revealCell (x, y) = do
board <- get
lift (putStrLn ("Trying cell " <> show (x, y))) #1
case Map.lookup (x, y) board of
Nothing -> lift (putStrLn "No such cell.")
Just (Revealed _) -> lift (putStrLn "Already revealed.")
Just (Hidden Bomb) -> lift (putStrLn "It's bomb! You loose!")
Just (Hidden c) -> do
let newBoard = Map.insert (x, y) (Revealed c) board
put newBoard
lift (putStrLn "Cell revealed.")
The listing contrasts direct state operations with IO operations that need lift, making the boundary between the two layers concrete. section 7.2.3
Stacks can add more than one StateT layer when separate state contexts deserve separate ownership. A game stack may keep Score at the top and Board beneath it. The nearest layer is accessed directly; reaching the lower board requires lift get, and reaching IO requires lift twice. This separation can model distinct concerns, but deeper stacks also add lifting ceremony. section 7.2.3 section 7.2.3
StateT computations are executed with runStateT, execStateT, or evalStateT. runStateT returns both result and final state inside the underlying monad; execStateT keeps only state; evalStateT keeps only the result. For a stacked computation, layers are run one at a time: a game state action can become a board action, then an IO action. That staged execution also supports tests that assert the resulting board. section 7.2.3 section 7.2.3
Listing 7.9 Running the monad stack in tests
board1 :: Board
board1 = Map.fromList
[ ( (1, 1), Hidden Empty )
, ( (2, 2), Hidden Bomb )
]
board2 :: Board
board2 = Map.fromList
[ ( (1, 1), Revealed Empty )
, ( (2, 2), Hidden Bomb )
]
spec :: Spec
spec =
describe "StateT monad tests" $ do
it "BoardMonad stack" $ do
let runBoard = runStateT (revealCell (1, 1)) board1 #1
(_, board') <- runBoard #2
board' `shouldBe` board2
it "MinesweeperGame stack" $ do
let runGame :: BoardMonad Score = execStateT printGame 0 #1
let runBoard :: IO Board = execStateT runGame board1 #1
board' <- runBoard #2
board' `shouldBe` board1
The tests run a BoardMonad and a two-state game stack, then check the final board after each layer is executed. section 7.2.3
StateT can also add model state to a domain-specific monad. A ship-control script uses StateT SpaceshipModel LogicControl so rotation code reads the current model directly while lifting property lookups and validation from LogicControl. The resulting action can later be evaluated back into LogicControl and executed to produce the changed model. When the underlying monad is a type synonym, wrapping it in a newtype may be necessary before using it as a transformer layer. section 7.2.3 section 7.2.3

