Another introduction to monads for very completely 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 have to convert objects to the correct type. Therefore, you first need to understand the rules of this game. When writing code, you must 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, things are not so obvious. Under the cut it is described in detail how to work with the do-design, 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 has the type “ma”, where “m” is a monad. In our case, this is the IO monad.

Each line in the do-structure also has the type "ma". The value of "a" in each line may be different.

The symbol "<-", as it were, converts the type of "IO String" to the type of "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 -> ma main = do text <- getLine doubleText <- return $ text ++ text putStr doubleText
The
return function wraps any type “a” into the monadic type “ma”.

In this example, using
return, an expression of the type “String” is converted to the type “IO String”, which is then turned back to “String”. Alternatively, the
let keyword can be used inside the do-construct.
main = do text <- getLine let doubleText = text ++ text putStr doubleText
All do-construction takes the last line type.

Suppose we want to read the contents of a file. To do this, we have a function
readFile .
readFile :: FilePath -> IO String
As you can see, the function returns "IO String". But we need the contents of the file as a "String". This means that we must perform our function inside the do-structure.
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 regular 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 within which you can perform some specific actions for a given space.
For example, in the monad “IO” you can display text in the console.

main = do print "Hello"
In the state monad there is some external state that we can modify.

main = do let r = runState (do modify (+1) modify (*2) modify (+3) ) 5 print r
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 sa -> s -> (a, s)
we "launch" our monad.

You can look at the monad from two sides: inside and outside. From the inside we can perform some actions specific to this monad. And from the outside - we can “launch” it, “print out” it, convert it to some non-monadic type.
This allows us to put one do-structure into another, as in the example above. The
IO monad is the only monad that cannot be looked at "outside." Everything ends up being nested in
IO . Monad
IO 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
monad transformers .
main = do r <- runStateT (do modify (+1) modify (*2) s <- get lift $ print s modify (+3) ) 5 print r
This program does the same as the previous one. We replaced
State with
StateT and added two lines:
s <- get lift $ print s
with the help of which we output the intermediate result to the console. Note that an I / O operation is performed inside the nested monad StateT.
Here,
runStateT starts the
StateT monad, and the
lift function “raises” the operation available in
IO to the
StateT monad.
runStateT :: StateT sma -> s -> m (a, s) lift :: IO a -> StateT s IO a
Study carefully how the type is sequentially converted in this example.

The “print s” operation is of type “IO ()”. With the help of
lift, we “raise” it to the “StateT Int IO ()” type. The internal do-structure is now of type “StateT Int IO ()”. We “launch” it and get the type “Int -> IO ((), Int)”. Then we substitute the value "5" and get the type "IO ((), Int)".
Since we have received the “IO” type, we can use it in the external do-structure. The arrow "<-" removes the monadic type and returns "((), Int)". The result "((), 15)" is displayed in the console.
Inside
StateT, we can change the external state and perform I / O operations. Those. the
stateT monad
does not “hang in the air” like
State , but remains connected to the outer
IO monad.

Thus, the program may be a bunch of monads nested in each other. Some of these monads will be linked to each other, some will not.
I hope my analogy has helped you to look at things from a new point of view, and you will be able to become a real master of monads in the future.
