Haskell: Monad transformers and global state
I am trying to learn Haskell. I'm trying to write a program that contains the "global state": Vars
. I want to change the state component (for example var1
) every time I call the function. The change can be a simple function on components (like +4). Also, it prints the changed component. Here is what I have done so far (but I am stuck). Edit: After running the code, I want to see the latest version of the global state.
import Control.Monad.State
import Control.Monad.IO.Class (liftIO)
data Vars = Vars {
var1 :: Int,
var2 :: Float
} deriving (Show)
sample :: StateT Vars IO a
sample = do
a <- change
liftIO $ print a
-- I want to call change again and apply more change to the state
change :: StateT Vars IO a
change = do
dd <- get
-- I don't know what to do next!
main = do
runStateT sample (Vars 20 3)
evalStateT sample (Vars 20 3)
source to share
Try to solve your problem step by step, starting with simple and small details. This is an important skill in programming and FP teaches you this skill in a good way. Also, working with a monad State
and especially multiple effects in monad transformers helps you reason about effects and understand things better.
-
You want to update
var1
inside your immutable data type. This can only be done by creating a new object. So write a function like this:plusFour :: Vars -> Vars plusFour (Vars v1 v2) = Vars (v1 + 4) v2
There are ways to write this function in Haskell that are much shorter, albeit less clear, but we don't care about those things right now.
-
Now you want to use this function inside
State
monad to update immutable state and by simulating mutability. What can you say about this function just by looking at its signature likechange :: StateT Vars IO a
:? We can say that this function has several effects: it has access to stateVars
and can perform arbitrary actionsIO
. Also this function returns a value of typea
. Hmm, this last one is weird. What is ita
? Which function should you return? In imperative programming, this function will be of typevoid
orUnit
. It just does something, it doesn't return everything. Only updates the context. Therefore, the type of the result must be()
. It might be different. For example, we can return a new oneVars
after the change. But this is generally a bad programming approach. This makes this function more complex. -
Once we understand what a type function should have (try to always start with defining types), we can implement it. We want to change our state. There are functions that operate on the partial state of our context. Basically, you are interested in this:
modify :: Monad m => (s -> s) -> StateT s m ()
modify
The function takes a function that updates the state. After running this function, you can observe that the state changes according to the passed function. Nowchange
it can be written as follows:change :: StateT Vars IO () change = modify plusFour
You can implement
modify
(and thereforechange
using only functionsput
andget
, which is a good exercise for beginners). -
Now let's call functions
change
from some other function. What does the challenge mean in this case? This means that you are performing a monadic actionchange
. This action changes your context, you don't care about it because it is()
. But if you run theget
function (which binds the integer state to the variable) afterchange
, you can watch the new change. If you only want to print the changed component, for examplevar1
, you can use the functiongets
. And again, what type should it besample
? What should he return? If on the caller side you are only interested in the resulting state, then, again, it should be()
like this:sample :: StateT Vars IO () sample = do change v1 <- gets var1 liftIO $ print v1 change v1' <- gets var1 liftIO $ print v1' -- this should be v1 + 4
This should give you some idea of โโwhat's going on. Monad transformers take some time to get used to, although it's a powerful tool (not ideal, but extremely useful).
As a side note, I want to add that this function can be written much better using the common Haskell design patterns. But you don't need to take care of them right now, just try to understand what's going on here.
source to share