|
🐱
StatefulKitten
Kitten
Posts: 234
OP
|
hi everyone (=^・ω・^=) i've been working through LYAH and i just hit the State monad chapter and i'm genuinely about to cry. i understand conceptually that it threads state through computations but i keep getting tripped up by the three runner functions. like, which one do i use when???
my mental model is breaking down. here's what i think they do:
-- runState: gives you BOTH the result AND the new state
runState :: State s a -> s -> (a, s)
-- evalState: gives you just the RESULT, throws away the state
evalState :: State s a -> s -> a
-- execState: gives you just the FINAL STATE, throws away result
execState :: State s a -> s -> s
is that right? and then composing stateful computations... i'm lost. like if i have two stateful actions how do i chain them? do i just use >>= ? here's a dumb example i tried:
import Control.Monad.State
addOne :: State Int ()
addOne = do
n <- get
put (n + 1)
doubled :: State Int ()
doubled = do
n <- get
put (n * 2)
-- then i try to chain them and get confused???
combined :: State Int ()
combined = addOne >> doubled
-- what does execState combined 3 give me?? is it 8 or something else?
i'm so confused. does addOne run first and pass its state to doubled? or does each one get the original state? i feel like i have no intuition for this. please help a sad kitten 😿 nya~
/ᐠ。ꞈ。ᐟ\
still learning Haskell, one cry at a time
"a monad is just a monoid in the category of endofunctors" - yeah thanks that helps
💜 Relatable 17
🐾 Same 5
😿 Sad 3
|
|
😺
LambdaWhisker
Senior Catgirl
Posts: 1,847
λ-kitty
|
StatefulKitten wrote:
what does execState combined 3 give me?? is it 8 or something else?
hey don't cry! this is one of the most confusing parts of Haskell for beginners, you're not alone nya~ 🐾
your type signatures up top are exactly right! to summarize clearly:
runState — gives you the pair (result, finalState)
evalState — gives you just the result (like fst . runState)
execState — gives you just the final state (like snd . runState)
and yes, >>= (or >> when you don't care about the value) chains stateful computations — crucially, the output state of the first becomes the input state of the second. This is the whole magic of the monad!
so for your example:
-- execState combined 3
-- Step 1: addOne runs with state=3 → state becomes 4, result=()
-- Step 2: doubled runs with state=4 → state becomes 8, result=()
-- execState discards result, returns final state: 8
ghci> execState combined 3
8
if you had run them the other way around:
combined2 :: State Int ()
combined2 = doubled >> addOne
-- doubled: 3 → 6, then addOne: 6 → 7
ghci> execState combined2 3
7
order matters! the bind operator >>= under the hood takes the final state from the first computation and feeds it as the initial state of the second. nya~
a slightly more concrete intuition: think of State s a as just a fancy wrapper around a function s -> (a, s). every time you sequence with >>=, you're threading that s through automatically so you don't have to pass it by hand.
/ᐠ-ꞈ-ᐟ\
LambdaWhisker :: ∀ a. a -> IO Catgirl
"types are the purrs of the compiler"
💜 Helpful 23
🐾 Thanks 11
✨ Smart 6
|
|
🐈
CurryCat
Combinator Kitty
Posts: 3,102
∘-cat
|
LambdaWhisker's answer is great, but I want to really demystify the State monad by showing you what it actually is. No magic, promise! (=^ᆺ^=)
At its heart, State s a is literally just a newtype wrapper around a function:
newtype State s a = State { runState :: s -> (a, s) }
That's it!! runState isn't some magical incantation — it's literally just the record accessor that unwraps the function inside! When you write runState myComputation initialState, you're applying the wrapped function to the initial state.
And evalState / execState are just conveniences:
evalState m s = fst (runState m s) -- just take the first element
execState m s = snd (runState m s) -- just take the second element
Now let's look at the Monad instance — this is where the threading magic lives:
instance Monad (State s) where
return a = State $ \s -> (a, s)
-- ^^^ just return the value, pass state unchanged
State act >>= k = State $ \s ->
let (a, s') = act s -- run first action, get result 'a' and new state 's'
in runState (k a) s' -- pass 'a' to continuation, run with new state
See? s' (the output state of act) becomes the input to k a. The state is threaded automatically. The do-notation just desugars >>= and >> calls, so all that threading happens invisibly.
The key insight: a value of type State s a isn't a result — it's a description of a computation that, when given an initial state, will produce a result and a new state. Nothing actually runs until you call one of the runners!
This is why people call Haskell "lazy" in more than just the evaluation sense — you build up a big description of what you want to happen, and then evaluate it all at once. nyaaa~
/ᐠ≧ω≦ᐟ\
CurryCat :: (a -> b) -> Cat a -> Cat b
curry . uncurry = id -- the most important theorem
💜 Enlightening 31
🧠 Big Brain 14
🐾 Nyaa 8
|
|
🐱
StatefulKitten
Kitten
Posts: 234
OP
|
OHHHH. ok. ok i think i get it now. it's literally a function wrapper and runState just calls the function!! i was imagining it as some kind of spooky global state box but it's just... functions. functions all the way down!!
let me try writing a slightly more useful example to make sure i understand:
import Control.Monad.State
-- keep a counter and a log of operations
type AppState = (Int, [String])
increment :: State AppState ()
increment = do
(n, log) <- get
put (n + 1, log ++ ["incremented to " ++ show (n+1)])
reset :: State AppState ()
reset = do
(_, log) <- get
put (0, log ++ ["reset!"])
program :: State AppState ()
program = do
increment
increment
increment
reset
increment
-- I want BOTH the final state and I don't care about the () result
main :: IO ()
main = do
let finalState = execState program (0, [])
print finalState
-- (1, ["incremented to 1","incremented to 2","incremented to 3","reset!","incremented to 1"])
is that right?? i used execState because i just want the final state and the program returns () anyway. if my stateful action returned something meaningful i'd use evalState or runState depending on whether i need the state too?
this is clicking!! thank you both so much 😸 nya~
/ᐠ。ꞈ。ᐟ\
still learning Haskell, one cry at a time
"a monad is just a monoid in the category of endofunctors" - yeah thanks that helps
💜 Progress! 9
🐾 Go kitten! 7
|
|
😸
PurelyFunctionalFelidae
Typed Tabby
Posts: 908
pure~
|
Great progress StatefulKitten! Your example looks correct. Let me add some more practical patterns that come up all the time with State:
Using modify instead of get+put:
-- modify applies a function to the current state
-- cleaner than get >>= \s -> put (f s)
addOne' :: State Int ()
addOne' = modify (+1)
double' :: State Int ()
double' = modify (*2)
-- gets projects out part of the state
getSquared :: State Int Int
getSquared = gets (^2)
ghci> evalState getSquared 5
25
Composing with sequence and replicateM:
-- add 1 five times
addFive :: State Int ()
addFive = replicateM_ 5 (modify (+1))
ghci> execState addFive 0
5
-- mapM_ to apply a stateful action to a list
sumList :: [Int] -> State Int ()
sumList xs = mapM_ (\x -> modify (+x)) xs
ghci> execState (sumList [1..10]) 0
55
When you want the result AND the state, use runState:
popStack :: State [Int] Int
popStack = do
stack <- get
case stack of
[] -> return (-1) -- empty stack sentinel
(x:xs) -> do
put xs
return x
ghci> runState (do { a <- popStack; b <- popStack; return (a+b) }) [3,5,7]
(8, [7])
-- result is 3+5=8, remaining stack is [7]
notice how runState is perfect for popStack because you care about both the popped value (the result) and what's left on the stack (the new state). nyaaa~ (=ↀωↀ=)
/ᐠ。⁰ᆺ⁰。ᐟ\
PurelyFunctionalFelidae
∀ f. Functor f => (a -> b) -> f a -> f b
purr-fectly typed
💜 Super Helpful 19
🐾 Bookmarked 12
✨ Elegant 7
|
|
🐾
MonadicMittens
Applicative Paw
Posts: 567
ap~
|
omg the stack example is so good for building intuition! also worth knowing about StateT — the monad transformer version — for when you need State layered on top of other effects (like IO):
import Control.Monad.State
-- StateT lets you combine State with IO (or any other monad)
verboseIncrement :: StateT Int IO ()
verboseIncrement = do
n <- get
lift $ putStrLn $ "Incrementing " ++ show n ++ " to " ++ show (n+1)
modify (+1)
main :: IO ()
main = do
finalN <- execStateT (replicateM_ 3 verboseIncrement) 0
putStrLn $ "Final: " ++ show finalN
-- Incrementing 0 to 1
-- Incrementing 1 to 2
-- Incrementing 2 to 3
-- Final: 3
note how execStateT is the transformer version of execState, and returns IO s instead of just s. lift is used to "lift" IO actions into the StateT context. super useful for real programs nya~
/>ᐠ- ˕ -マ\
MonadicMittens
lift . lift . lift -- cats all the way up
💜 Useful 15
🐾 Nice 6
|
|
✨
EffectfulKitten
Effect Handler
Posts: 412
eff-cat
|
hot take: just use algebraic effects 😼
like, State monad is fine and you should learn it, but once you've internalized what it does you might appreciate that algebraic effect systems (like in Polysemy, Effectful, or fused-effects) handle this more ergonomically for real programs:
-- with the `effectful` library
import Effectful
import Effectful.State.Static.Local
myProgram :: (State Int :> es) => Eff es ()
myProgram = do
modify (+1)
modify (*2)
n <- get
liftIO $ print n
the syntax is almost the same as State monad but it composes with other effects without needing transformer stacks. you can just list the effects you need in the constraint: (State Int :> es, Reader Config :> es, IOE :> es) => Eff es a
but don't skip learning the State monad! you need to understand it to understand algebraic effects. it's turtles all the way down nya~ 🐢
/ᐠ ̥ ̮ ̥ᐟ\
EffectfulKitten :: (Eff :> es) => Eff es Catgirl
my monad transformer stack is just: IO
💜 Spicy Take 11
😾 Controversy 8
🐾 Fair Point 9
|
|
🐱
STmonadSiamese
Mutable Tabby
Posts: 789
ST~
|
since we're talking about stateful computation in Haskell, should probably also mention ST and IORef since they come up when people search for "State monad" and can cause confusion:
| Tool |
Use when... |
Pure? |
State s a |
Purely functional stateful computation |
✓ Pure |
ST s a |
Need actual mutable refs but want purity guarantee |
✓ Pure (runST) |
IORef a |
Mutable state inside IO, don't need to escape |
✗ Impure (IO) |
MVar a |
Concurrent mutable state / communication |
✗ Impure (IO) |
The ST monad is cool because it lets you use actual heap-allocated mutable references (STRef) for performance, but runST escapes them safely — the type system uses a rank-2 type trick with the s phantom parameter to prevent refs from leaking out!
import Control.Monad.ST
import Data.STRef
pureSum :: [Int] -> Int
pureSum xs = runST $ do
ref <- newSTRef 0 -- create mutable ref
mapM_ (modifySTRef ref . (+)) xs
readSTRef ref -- read final value
-- pureSum is a pure function despite using mutation internally!
for most beginner stuff, stick with State. ST / STRef is a performance optimization when you need actual O(1) mutations. nya~
/ᐠ^•ω•^ᐟ\
STmonadSiamese
runST :: (∀ s. ST s a) -> a
-- that forall is doing SO much work
💜 Educational 22
🧠 Reference! 16
🐾 Saved 9
|
|
✨
EffectfulKitten
Effect Handler
Posts: 412
eff-cat
|
STmonadSiamese wrote:
The ST monad is cool because it lets you use actual heap-allocated mutable references (STRef) for performance, but runST escapes them safely...
yes!! the ∀ s. trick in runST :: (∀ s. ST s a) -> a is genuinely one of the most elegant things in all of type theory. the rank-2 polymorphism makes it impossible to let an STRef escape the computation — the type system literally enforces purity. it's beautiful 😻
also +1 to the IORef mention — for beginners who come from imperative languages, sometimes IORef is the most straightforward path and that's OK!
-- totally valid if you're already in IO
import Data.IORef
main :: IO ()
main = do
counter <- newIORef (0 :: Int)
modifyIORef counter (+1)
modifyIORef counter (+1)
val <- readIORef counter
print val -- 2
just don't reach for it when State would do — you lose purity and testability for no good reason. nya~
/ᐠ ̥ ̮ ̥ᐟ\
EffectfulKitten :: (Eff :> es) => Eff es Catgirl
my monad transformer stack is just: IO
💜 Agree 8
🐾 7
|
|
🔮
GaloisCat
Category Theorist
Posts: 2,441
∀-nyan
|
OK I can't lurk on this thread without dropping some category theory. don't run away, I promise this is actually useful (=^‥^=)
The State monad as a special case of the Continuation monad:
The continuation monad has type Cont r a = (a -> r) -> r. What happens if we let the result type r be a function type — specifically, r = s -> (a, s)? That gives us...
-- The continuation monad:
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
-- If we set r ~ (s -> (b, s)), then Cont r a becomes:
-- (a -> (s -> (b, s))) -> (s -> (b, s))
-- Which is just the CPS encoding of:
-- a -> s -> (b, s)
-- i.e., a stateful computation! Hence State s a ≅ Cont (s -> (a, s)) ...
-- More precisely, we can derive State from Cont:
type StateViaCont s a = forall r. Cont (s -> (r, s)) a
-- the usual get and put still work:
getCont :: StateViaCont s s
getCont = Cont $ \k s -> k s s
putCont :: s -> StateViaCont s ()
putCont s' = Cont $ \k _ -> k () s'
This is a specific instance of a general phenomenon: many common monads are specializations of the continuation monad Cont r for particular choices of r:
r = () → Identity monad
r = (w, s) → Writer w monad (via reverse state)
r = Maybe b → Maybe monad (abort/exception)
r = s -> (b, s) → State s monad
r = [b] → List monad (nondeterminism)
The continuation monad is sometimes called the "mother of all monads" for this reason — you can derive almost any monad as a specialization of it. The category theoretic reason is that Cont r is the monad arising from the double-negation translation into r.
Whether this perspective is "useful" is debatable in practice, but it's one of those things where once you see it, every monad starts to feel less magical and more like a specific tool for a specific shape of computation. nyaaaa~ ✨
/ᐠ≽^•⩊•^≼ᐟ\
GaloisCat
Hask is not a category (but pretend it is)
"Every monad is a monoid in the category of endofunctors. Deal with it." nyaa~
💜 Galaxy Brain 28
🧠 Math Kitty 19
😵 Mindblown 14
🐾 Bookmarked 11
|
|
🐈
CurryCat
Combinator Kitty
Posts: 3,102
∘-cat
|
GaloisCat wrote:
The continuation monad is sometimes called the "mother of all monads" for this reason...
love this!! to bring it back to earth for OP though — the practical upshot is that if you ever feel confused about what a monad is "doing," ask yourself: "what is this monad doing with its continuations?"
For State: the continuation gets an extra state argument, and threads it through. That's literally it!
Also want to add one more practical tip for OP — gets is your friend when you have a record-typed state and want to project a field:
data GameState = GameState
{ score :: Int
, lives :: Int
, position :: (Int, Int)
} deriving (Show)
getScore :: State GameState Int
getScore = gets score -- project the score field
addScore :: Int -> State GameState ()
addScore n = modify (\gs -> gs { score = score gs + n })
loseLife :: State GameState Bool -- returns True if game over
loseLife = do
modify (\gs -> gs { lives = lives gs - 1 })
remaining <- gets lives
return (remaining <= 0)
this pattern — big record as state, gets for reading fields, modify with record update syntax for writing — is extremely common in real Haskell code. combined with lenses (see the lens thread) it gets very ergonomic. nya~
/ᐠ≧ω≦ᐟ\
CurryCat :: (a -> b) -> Cat a -> Cat b
curry . uncurry = id -- the most important theorem
💜 Practical 17
🐾 Real World! 10
|
|
😺
LambdaWhisker
Senior Catgirl
Posts: 1,847
λ-kitty
|
quick summary post for OP (and anyone finding this thread via search 🐾):
The three runners — when to use each:
-- Use runState when you need BOTH the result and final state
runState :: State s a -> s -> (a, s)
-- Use evalState when the STATE is just scaffolding; you want the VALUE
-- e.g. "run this computation, give me the answer, I don't care about state"
evalState :: State s a -> s -> a -- = fst . runState
-- Use execState when the VALUE is just scaffolding; you want the STATE
-- e.g. "build up this data structure, I don't care about intermediate values"
execState :: State s a -> s -> s -- = snd . runState
-- Mnemonic:
-- eval → evaluate the expression → gives you the VALUE
-- exec → execute the side effect → gives you the STATE
-- run → run everything → gives you BOTH
Essential combinators:
get :: State s s -- read state as value
put :: s -> State s () -- replace state, return ()
modify :: (s->s) -> State s () -- transform state
gets :: (s->a) -> State s a -- read + project
hope the thread helped StatefulKitten! you clearly get it now based on your example in post #4 — that was spot on ✨ nya~
/ᐠ-ꞈ-ᐟ\
LambdaWhisker :: ∀ a. a -> IO Catgirl
"types are the purrs of the compiler"
💜 Perfect Summary 25
📌 Pinned Mentally 18
🐾 Thank You 13
|
|
🐱
StatefulKitten
Kitten
Posts: 234
OP
|
omg you all are INCREDIBLE. this thread is so good!! let me try to write the most concise possible summary of what i learned:
State s a is just a newtype around s -> (a, s) — a function!
- The monad instance threads state by feeding output state of one action as input state of the next
runState / evalState / execState are just ways to unwrap the result
- Use
modify + gets for cleaner code, not always raw get+put
- For real apps:
StateT lets you combine with IO; algebraic effects for more complex stacks
- Continuation monad is the spooky generalization underneath everything 😸
i'm going to go rewrite my little project with a proper State monad instead of manually threading state through every function. gonna be so much cleaner!! nya~ 🐾
[ THREAD RESOLVED ✓ ]
/ᐠ。ꞈ。ᐟ\
still learning Haskell, one cry at a time
update: fewer cries now 😸
💜 Growth! 20
🐾 Go kitten!! 15
🎉 Solved 12
|
|
🔮
GaloisCat
Category Theorist
Posts: 2,441
∀-nyan
|
one last thing I can't resist adding: the relationship between State and the categorical adjunction. sorry not sorry (=^‥^=)
from category theory, the State monad arises from the adjunction between the product functor (-, s) and the exponential functor (s -> -). In Haskell terms, this means:
-- The adjunction: (a, s) -> b ≅ a -> (s -> b)
-- is just... curry/uncurry!
curry :: ((a, s) -> b) -> a -> s -> b
uncurry :: (a -> s -> b) -> (a, s) -> b
-- The monad generated by this adjunction is exactly State s!
-- The composition of (s ->) after (,s) is: a -> (s -> (a, s))
-- Flip the arguments and you get: s -> (a, s)
-- That's just runState!!! 🎉
moral of the story: currying is literally the State monad. Haskell is beautiful and I need to lie down nyaaaa~
See also: Adjunctions give rise to monads — the full story
/ᐠ≽^•⩊•^≼ᐟ\
GaloisCat
Hask is not a category (but pretend it is)
"Every monad is a monoid in the category of endofunctors. Deal with it." nyaa~
💜 Transcendent 21
😵 Send help 17
🧠 Cursed Math 13
🐾 nyaa 9
|