📜 ⬆️ ⬇️

Haskell. Monads. Monad transformers. Game types

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 -- 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 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 -- 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 

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.

Source: https://habr.com/ru/post/315022/


All Articles