Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I actually execute a StateT monad along with IO?

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.

like image 478
Savanni D'Gerinel Avatar asked Jul 06 '12 13:07

Savanni D'Gerinel


1 Answers

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? =)

like image 152
Daniel Wagner Avatar answered Nov 15 '22 07:11

Daniel Wagner