Back to Home

Haskell. Monads. Monad transformers. Game of types

Haskell · Monads · Monad Transformers

Haskell. Monads. Monad transformers. Game of types

  • Tutorial
Another introduction to monads for very beginners.

The best way to understand monads is to start using them. You need to score on monadic laws, category theory, and just start writing code.

Writing Haskell code is like a game in which you must convert the objects to the correct type. Therefore, you first need to understand the rules of this game. When writing code, you should clearly understand what type each particular piece of code has.

With normal functions, everything is clear. If there is a function of type “a-> b”, then substituting an argument of type “a” into it, you will get a result of type “b”.

With monads, everything is not so obvious. Under the cat, it is described in detail how to work with the do-construction, how types are sequentially converted, and why monad transformers are needed.

1. Do-design

Let's start with a simple example.

main = do 
	putStr "Enter your name\n"
	name <- getLine	
	putStr $ "Hello " ++ name

Each do construct is of type ma, where m is a monad. In our case, this is the IO monad.



Each line in the do-construction is also of type “ma”. The value of “a” in each line may be different.



The character "<-", as it were, converts the type "IO String" into the type "String".

If we need to perform some calculations in the monad that are not related to this monad, then we can use the return function .

return :: a -> m a
main = do 
	text <- getLine 
	doubleText <- return $ text ++ text
 	putStr doubleText 

The return function wraps any type "a" in the monadic type "ma".



In this example, using return, an expression of type “String” is converted to type “IO String”, which is then expanded back to “String”. Alternatively, you can use the let keyword inside a do construct .

main = do 
	text <- getLine 
	let doubleText = text ++ text
 	putStr doubleText

The whole do-construction takes the type of last line.



Suppose we want to read the contents of a file. For this, we have a readFile function .

readFile :: FilePath -> IO String

As you can see, the function returns an "IO String". But we need the contents of the file as a "String". This means that we must execute our function inside the do-construct.

printFileContent = do
	fileContent <- readFile "someFile.txt" 
	putStr fileContent

Here the fileContent variable is of type “String”, and we can work with it as with a normal string (for example, display it on the screen). Note that the resulting printFileContent function is of type "IO ()"

printFileContent :: IO ()

2. Monads and monad transformers

I will give the following simple analogy. Imagine that a monad is a space inside which you can perform some actions specific to a given space.
For example, in the IO monad, you can output text to the console.


main = do 
	print "Hello"

In the monad "State" there is some external state that we can modify.


main = do 
	let r = runState (do 
		modify (+1)
		modify (*2)
		modify (+3)
		) 5
	print r
-- OUTPUT: 
--      ((), 15)

In this example, we took the number 5, added 1 to it, multiplied the result by 2, then added another 3. As a result, we got the number 15.

Using the runState function

runState :: State s a -> s -> (a, s)

we are “launching” our monad.



The monad can be viewed from two sides: inside and out. From the inside, we can perform some actions specific to this monad. And outside - we can "launch" it, "print it out", convert it to some non-monadic type.

This allows us to embed one do construct in another, as in the example above. The IO monad is the only monad that cannot be looked at from the outside. Everything ultimately turns out to be embedded in IO . The IO monad is our foundation.

The above example has certain limitations. Inside the State monad, we cannot perform the actions available in IO .



We were "suspended in the air", lost contact with the ground.

To solve this problem, there are monadic transformers .

main = do 
	r <- runStateT (do 
		modify (+1)
		modify (*2)
		s <- get
		lift $ print s 
		modify (+3)
		) 5
	print r
-- OUTPUT:
--    12 
--    ((), 15)

This program does the same as the previous one. We replaced State with StateT and added two lines:

s <- get
lift $ print s 

using which we display the intermediate result in the console. Note that the I / O operation is performed inside the “nested” StateT monad.

Here runStateT launches Monad StateT , as a function of lift «raises" operation available in the IO , to monads StateT .

runStateT :: StateT s m a -> s -> m (a, s)
lift :: IO a -> StateT s IO a

Examine carefully how the type is sequentially converted in this example.



The operation "print s" is of type "IO ()". With the help of lift we “raise” it to the type “StateT Int IO ()”. Internal do-construction now has type “StateT Int IO ()”. We “run” it and get the type “Int -> IO ((), Int)”. Then we substitute the value “5” and get the type “IO ((), Int)”.
Since we got the type “IO”, we can use it in an external do-construct. The arrow "<-" removes the monadic type and returns "((), Int)". The result "((), 15)" is output to the console.

Inside StateT, we can change the external state and perform I / O operations. Those. StateT monad does n't “hang in the air” like State, but remained connected with the external monad IO .



Thus, the program may have a bunch of monads nested in each other. Some of these monads will be linked to each other, some not.

I hope my analogy has helped you look at things from a new perspective, and you will be able to become a real master of monads in the future.

Read Next