I am trying to follow the advice given in Combine state with IO actions for building up an AppState along with an IO monad. What I've gotten is this:
module Main where
import Control.Monad.State
import Control.Monad.Trans
data ST = ST [Integer] deriving (Show)
type AppState = StateT ST IO
new = ST []
append :: Integer -> State ST ()
append v = state $ \(ST lst) -> ((), ST (lst ++ [v]))
sumST :: State ST Integer
sumST = state $ \(ST lst) -> (sum lst, ST lst)
script = do
append 5
append 10
append 15
sumST
myMain :: AppState ()
myMain = do
liftIO $ putStrLn "myMain start"
let (res, st) = runState script new
liftIO $ putStrLn $ show res
liftIO $ putStrLn "myMain stop"
main = runStateT myMain (ST [15])
There's some part of this I'm not getting. It bothers me greatly that I have script
and myMain
and main
. It also bothers me that I have to execute runState
within myMain
and that I have to feed an initial state into runStateT
in my main function. I'm wanting to have my "script", so to speak, directly in the myMain function because the entire point of myMain is to be able to run the append and sum directly in myMain and right next to the print operations. I think I should be able to do this, instead:
myMain :: AppState ()
myMain = do
liftIO $ putStrLn "myMain start"
append 5
append 10
append 15
r <- sumST
liftIO $ putStrLn $ show res
liftIO $ putStrLn "myMain stop"
main = runState myMain
I had thought that the point of the monad transformer was so I can execute my State monad operations in a function (like above) and lift IO operations into that function. What is the right way to set all of this up so that I can remove one of the layers of indirection?
In addition to Daniel's solution (which I have flagged the solution), I have also found a few variations that might also shed some light on the situation. First, the final implementation of myMain and main:
myMain :: AppState ()
myMain = do
liftIO $ putStrLn "myMain start"
append 5
append 10
append 15
res <- sumST
liftIO $ putStrLn $ show res
liftIO $ putStrLn "myMain stop"
main = runStateT myMain new
Now, various implementations of append and sumST, in addition to Daniel's:
append :: Integer -> AppState ()
append v = state $ \(ST lst) -> ((), ST (lst ++ [v]))
sumST :: AppState Integer
sumST = state $ \(ST lst) -> (sum lst, ST lst)
and (note that only the type declaration changes; in fact you can omit the type declaration completely!)
append :: MonadState ST m => Integer -> m ()
append v = state $ \(ST lst) -> ((), ST (lst ++ [v]))
sumST :: MonadState ST m => m Integer
sumST = state $ \(ST lst) -> (sum lst, ST lst)
It occurred to me that the AppState/StateT monad is not the same as the basic State monad, and I was coding both sumST and append for the State monad. In a sense, they also had to be lifted into the StateT monad, though the correct way of thinking of in is that they had to be run in the monad (hence, runState script new
).
I'm not sure I completely get it, but I will work with it for a while, read the MonadState code, and write something about this when it finally works in my head.
The problem is that you made your append
and sumST
functions too monomorphic! Instead of directly using the state
function, you should use the more polymorphic get
and put
functions, so that you can give them the more exciting types
append :: MonadState ST m => Integer -> m ()
append v = do
ST lst <- get
put (ST (lst ++ [v]))
sumST :: MonadState ST m => m Integer
sumST = do
ST lst <- get
return (sum lst)
Then you can write exactly the myMain
you proposed (though you'll still have to give an initial state in main
).
As a stylistic thing, I would propose not defining a new ST
type: there are lots of functions that do handy things with lists, and making them impossible to use by imposing a ST
constructor in between you and the lists can be annoying! If you use [Integer]
as your state type instead, you can make definitions like this:
prepend :: MonadState [Integer] m => Integer -> m ()
prepend = modify . (:)
sumST :: MonadState [Integer] m => m Integer
sumST = gets sum
Looks pretty nice, no? =)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With