📜 ⬆️ ⬇️

Another Monad Guide (part 2: >> = and return functions)

By mike vanier

Two fundamental monadic operations


Remember, I said that monads generalize composition and application of functions? Here we will talk about it here. Be patient, we will need some time.

By this time, I hope you have at least a vague feeling of the monads, what they are and what they are used for. I have already mentioned one of the features of functional programming - the composition of functions, thanks to which we create new functions by combining the old ones. Functional programmers are constantly talking about "combinability" {1}, meaning that if something in the programming language is not combined, then it costs little. Similarly, our newly appeared monadic functions would not be as useful if they were not arranged as they really are. But we still will see, for their composition it is impossible to use the standard function “dot” (.) Of the Haskell language. We will come to the conclusion that something more is needed here, and we define two fundamental monadic operations (or, for a start, their types).

Suppose we have such monadic functions:
')
f :: a -> m b
g :: b -> m c


for some monad m . If you want a more specific example, you can imagine that f and g are functions in the IO monad:

f :: a -> IO b
g :: b -> IO c


However, the same is true for other monads. Recall (for the case of IO ) that the function f takes a value of type a and outputs a value of type b , and in the process, it may interact with input / output ( IO ). The function g , in turn, takes a value of type b and outputs a value of type c , and in the process, perhaps, works with input / output. Using the composition of these functions, we hope to get the function h :

h :: a -> IO c


that is, the result is a function that takes a value of type a , displays a value of type c, and in the process works with I / O (where I / O is a combination of what f and g do ). In pseudo-language, we could write it like this:

connect function:
( f :: a -> IO b )
with function:
( g :: b -> IO c )
To obtain:
( h :: a -> IO c )


However, in Haskell, the composition operator (dot) knows nothing about the type of IO , so it will not work with our functions f and g . For comparison, let's look at pure functions with ordinary types p , q , r without any IO :

p :: a -> b
q :: b -> c
r :: a -> c


Operators (.) And (>.>) Are suitable here:

( . ) :: ( b -> c ) -> ( a -> b ) -> ( a -> c )
r = q . p
( >.> ) :: ( a -> b ) -> ( b -> c ) -> ( a -> c )
r = p >.> q


But none of them will work with monadic functions:

f :: a -> IO b
g :: b -> IO c
h :: a -> IO c
g . f -> typing error ! Mismatch between IO b and b
f >.> g -> typing error ! Mismatch between IO b and b


You cannot use a monadic value of type IO b if type b is required. (This is the most common error of monadic programs in Haskell.) We need a special function of monadic composition, which I call mcompose (from "monadic compose"). She has the following type:

mcompose :: ( a -> m b ) -> ( b -> m c ) -> ( a -> m c )


It works for any monad, including the IO monad. Substituting IO , we get the corresponding function definition:

mcompose :: ( a -> IO b ) -> ( b -> IO c ) -> ( a -> IO c )


We could use it to compose the monadic functions f and g . The type of the function h would be correct:

f :: a -> IO b
g :: b -> IO c
h :: a -> IO c
h = f `mcompose` g


(Here, the mcompose function is surrounded by inverse apostrophes. This is elegant syntactic sugar, which allows you to make an infix operator from a two-argument function. Do not forget that the operators in Haskell are simply functions placed between their operands.) The mcompose function (or operator if you want) is capable of:

  1. take an initial value of type a ;
  2. apply the function f to it (the usual application of functions) and get the result of type IO b ;
  3. take a value of type IO b from function f and extract the value of type b (this is what we cannot do);
  4. take the value of type b and apply the function g to it (again, the usual application of functions) to get the value of type IO c , which is the desired result.


The only thing we can not do yet is step (3), to get a value of type b from a value of type IO b . Let's come up with an extract function that we could use to extract. Here is its type:

extract :: IO b -> b


And if we generalize to all monads, we get:

extract :: m b -> b


It turns out that if such a function existed, it would level off all the advantages of monads and pure functional programming! There is a reason we need monads. We want to keep special concepts of computation (monadic functions) separate from pure functions, because otherwise there would be no guarantee that pure functions are pure. This is an important point, and I'm going to spend some time on it, and then we will return to the monadic composition.

* Note on the margins. * In fact, for some monads there is an equivalent of the extract function, which does not entail any problems. However, I must say that the extract function generalized to all monads is prohibited.


We would like to know for sure that functions without monadic types are pure. Although generally in Haskell, monadic functions are pure, because they are made as pure functions that return a monadic value. But we want to ensure that non-monadic (pure) functions will not even try to work with monadic values. Then they will surely be clean. For example, pure function hh type

hh :: a -> c


will never invoke a read / write operation (with a file or console), because otherwise it would have had type

hh :: a -> IO c


Such guarantees, backed by a type system, are one of Haskell’s main strengths. They allow us, one look at the definition of a function, to be 100% sure that it does not interact with input / output (for example).

However, if we had the extract function, we could compose hh , a supposedly clean function from unclean I / O operators:

ff :: a -> IO b
gg :: b -> c
hh = ff >.> extract >.> gg - or the same: hh = gg. extract. ff


Even if it was never assumed that the hh function would do input / output, you could create it using extract and the usual composition operator, and the type hh would be clean — but inside it would still perform input / output operations. And it would not be possible to separate IO (as well as other monadic calculations) from pure calculations. (But this is one of the main reasons for the introduction of monads.) Note, by the way, that this situation is exactly the same with most conventional programming languages, which is why their type systems do not guarantee that the functions are clean. Unlike Haskell, we like its mechanism for pure functions, its type system, which requires pure functions to be pure, which is why there is no extract function in Haskell.

There is one small problem with what I just said: in general, this is a lie. There is an unsafePerformIO function with type IO a -> a , that is, this is the extract version for the IO monad. The word “unsafe” hints that you should avoid this function if you don’t know what exactly you want to do, or are not ready for strange malfunctions. I have never had to use unsafePerformIO , but there are legal cases, for example, deep in the implementation of Haskell compilers. Just forget that I told you this, ok? I feel embarrassed because of this. Excuse me. {3}

But - we will continue. At this point, we have established: (a) - the composition of monadic functions is needed; (b) - the usual composition operator is not suitable for this, because we cannot reduce monadic types to ordinary ones; and (c) - you cannot set the extract function, because it will spoil the purity of the whole language. So what do we do?

Well, first of all, let's try to invent something simpler than mcompose . Let's say a certain function mapply (monadic application), which has the following type:

mapply :: m b -> ( b -> m c ) -> m c


And if we descend from common monads to IO , we get:

mapply :: IO b -> ( b -> IO c ) -> IO c


It is called mapply because of its similarity to the usual operator using functions. Recall, for example, the > $> operator, previously defined in a similar way:

( > $> ) :: b -> ( b -> c ) -> c


Same as mapply , only there is no “m” (types are not monadic). mcompose is trivially expressed through mapply :

mcompose :: ( a -> m b ) -> ( b -> m c ) -> ( a -> m c )
mcompose f g x = ( f x ) `mapply` g - or: mapply (f x) g


Since the arrow ( -> ) in the type definition is right associative, we can omit the parentheses of the last element:

mcompose :: ( a -> m b ) -> ( b -> m c ) -> a -> m c
mcompose f g x = ( f x ) `mapply` g


Maybe this version of mcompose is easier to understand than the previous one, but they are the same thing. It is worth noting that x is of type a , and the type of result is mc . Here is what we do here: we apply the function f to x to get a value of type mb ; then we pass this value (of type mb ) and the function g to mapply , thus obtaining the value of type mc we are interested in. It turns out that we do not need to have the mcompose function somewhere ; if we already have a mapply , through it we can write mcompose ourselves . And, in fact, mapply is one of two fundamental monadic operations. It is usually called “bind” (“bind”) and is written as a character infix operator >> = :

( >> = ) :: m a -> ( a -> m b ) -> m b


It is worth noting that I slightly changed the definition of the type: I replaced b with a and c with b . But this is not important, since a , b , c are type variables, they work with any types.

I would like to emphasize that the >> = operator is extremely abstract. Its first argument is a value of type ma , where a can be any type at all, and m any monadic type constructor. The second argument is the function a -> mb , where a and b are also any types, and m is, again, any monadic type constructor. Finally, the return value is of type mb , where b can be any type and m can be any monadic type constructor. (For experienced Haskell programmers, such type definitions become second nature, but for newbies this can be difficult.) You can specialize the definition up to the IO monad, and get a monadic application operator on the IO monad:

( >> = ) :: IO a -> ( a -> IO b ) -> IO b


We will soon see that the Haskell type system allows the use of the generic >> = operator for a wide variety of monads (is that cool, right?).

Suppose that we have the operator >> = , with it we can connect f and g to get h :

- suppose we are given:
f :: a -> m b
g :: b -> m c

- definition h:
h :: a -> m c
h x = f x >> = g


We can also rewrite h in a different way:

h = \ x -> f x >> = g


where \ x -> ... is, as mentioned earlier, the designation of anonymous functions in Haskell {4} (in this case with one argument x ); both versions of h mean the same thing. Using mcompose , we can write the following equation:

h = f `mcompose` g = mcompose f g = \ x -> ( f x >> = g )


That is, our mcompose is defined as

mcompose f g = \ x -> ( f x >> = g )


In fact, Haskell already has the standard monadic composition operator > => :

f > => g = \ x -> ( f x >> = g ) is the same as (f `mcompose` g) but shorter.


Assuming that we have a monadic application operator >> = , we could easily specify a monadic composition operator > => . Hence, the monadic application operator (bind-operator) is conceptually important. Further we will see that for each monad a specific operator >> = is defined, which differs from all the others; This task is very well solved by the Haskell type classes. By the way, in the GHC compiler, the operator > => is defined in the Control.Monad module.

Now let's remember that we recorded the usual use statement in two ways:

( $ ) :: ( a -> b ) -> a -> b


and

( > $> ) :: a -> ( a -> b ) -> b


Which of these operators to apply depends on the order in which we wanted to pass the arguments. (It is great to define both operators in order to use them when it is convenient.) We can also write the monadic operator of application in two ways. The first method is the bind-operator >> = with type

( >> = ) :: m a -> ( a -> m b ) -> m b


which is analogous to the usual use statement > $> . A monadic application operator is given trivially, taking arguments in the reverse order:

( = << ) :: ( a -> m b ) -> m a -> m b
f = << x = x >> = f


You can also take the flip function, which takes a function of two arguments and returns the same function, but with the reverse order of the arguments:

flip :: ( a -> b -> c ) -> ( b -> a -> c )
flip f = \ x y -> f y x


Operator = << through it is defined as:

( = << ) = flip ( >> = )


You will get extra functional grade points {5} if your definitions are as brief as this.

And again: we can specify a monadic composition operator for the reverse order of the operands:

( > => ) :: ( a -> m b ) -> ( b -> m c ) -> ( a -> m c ) - already

( <= < ) :: ( b -> m c ) -> ( a -> m b ) -> ( a -> m c )
( <= < ) = flip ( > => )


So, we have defined monadic operators of application and composition for any order of operands, as we did with ordinary (non-monadic) operators. In practice, however, Haskell programmers use the >> = operator most of all (or, at least, I use it most of all).

If you understood all this, congratulations! All the hardest is over. I hope. {6}

There is another fundamental monadic operation that I am going to talk about. For seed, consider the following scenario. Suppose you need to combine the monadic function with a non-monadic one. In other words, you have these functions:

f :: a -> m b - monadic
g :: b -> c - nonmonadic


The problem is as follows. You cannot use the normal composition function for f and g , because mb is not the same as b . And the monadic composition also does not fit, - it knows nothing about the type b -> c , it needs the monadic type b -> mc . What are you going to do?

If you had the extract function parsed earlier, you could combine the two functions in this way:

h :: a -> c
h = f >.> extract >.> g


but we have already found out that this is impossible. That is, we are forbidden to combine the non-monadic function and the monadic function in order to obtain a non-monadic function (because it would violate the purity of the entire language). And we are allowed to combine monadic and nonmonadic, if the result is again a monadic function. Like this:

h :: a -> m c
h = f [ somehow combine with ] g


Well, we know that monadic composition is no good, because g has an incorrect type (which must be b -> mc ). However, it would be useful if we could convert a regular function to a monadic one. Let the function that performs such a transformation be called functionToMonadicFunction .

functionToMonadicFunction :: ( b -> c ) -> ( b -> m c )


The function h can now be written differently:

h :: a -> m c
h = f > => ( functionToMonadicFunction g )


It turns out that all that is needed is to define a function functionToMonadicFunction , and this is very simple, because a monadic function already exists with the name (possibly confusing) return . She has the following type:

return :: a -> m a


where a is any type, and m is any monadic type constructor. The return function converts a normal value to the corresponding monadic value for any monad m , and that is all it does. Here we are now using it.

If you have a return , then functionToMonadicFunction is expressed through it simply:

functionToMonadicFunction :: ( a -> b ) -> ( a -> m b )
functionToMonadicFunction f = \ x -> return ( f x )


Or, if you want to be cool, you can use the composition of functions:

functionToMonadicFunction :: ( a -> b ) -> ( a -> m b )
functionToMonadicFunction f = return . f


Or even:

functionToMonadicFunction :: ( a -> b ) -> ( a -> m b )
functionToMonadicFunction = ( return . )


The last example involved a great Haskell feature called "sections." All three options are equivalent.

Notice that I replaced the type letters again: b with a and c with b ; what letter will be in type does not matter. The main idea of ​​this is that with the help of return we can connect monadic and nonmonadic functions, getting monadic again. return is the second fundamental monadic operation.

* Marginal notes. * If you program mostly in an imperative style, the word return may seem somewhat irritating to you. Just remember that this is not a keyword in Haskell, and nothing returns from the function. Try not to think about return as return in an imperative programming language.


The name "return" actually came from the understanding of monadic values ​​as "actions". In this sense, the return function takes a simple value and produces a monadic value. This monadic value is already “doing something”, and the result will be the original value. It is also worth noting that in fact return is a monadic function. Putting these two thoughts together, you can conclude (or at least guess) that return is a monadic version of the identity function (this function maps the value to itself, that is, \ x -> x ). We will return to this when we talk about monad laws.

Let's take the return into circulation by connecting our monadic function f with the nonmonadic function g to get the monadic h .

h = f > => ( return . g )


And this is the correct entry, since return. g converts g to a monadic function.

After all that has been said, you are probably wondering how many more monadic operations we will have to plow before we define them all. As usual Professor Farnsworth used to say: “Good news!” There are only two of them! For convenience, we have set up some more not so important operations, but of them only >> = and return must be.

There is another rather peculiar moment with return . It is said that the return type looks like a -> ma . When we say, for example, return 10 , what will be the type of this expression? It can be IO Int , Maybe Int, or some other monadic Int . How do we know what a monad is worth? After all, the monadic value of IO Int is not at all the same as Maybe Int ; so we are not just interested in the right type, we want to understand what this value is - return 10 !

In Haskell, the meaning of return 10 is determined by the context. The type validator (type checker) must make sure that the functions take arguments with the necessary types, so if return 10 is passed to the function where IO Int is expected, then return 10 will be treated as IO Int . (The same rule holds true for other monads.) In other words, the value of return 10 depends on the type in the context of which it is used. If you wish, you can explicitly specify the type of return 10 expression using, for example, a record ( return 10 :: IO Int ), but this is rarely necessary.

Let's sum up what was said.



And what does monadic application and monadic composition really mean ?


At this point you should already understand the mechanisms of monadic composition and monadic application, but this is not the same as intuitively understanding their meaning. Let's try to clarify the situation.

So, we said that it is impossible to set the function extract , which would do the usual from a monadic value. However, if we need to combine two monadic functions into a third one, somehow we will still have to extract the usual values ​​from the monadic functions.

( > => ) :: ( a -> m b ) -> ( b -> m c ) -> ( a -> m c )
f > => g = {- something -}


Here it is shown: the function f takes a value of type a and returns a monadic value of type mb ; The g function takes a value of type b and returns a value of type mc . And this, of course, looks as if somewhere inside the usual meaning is “unpacked” from the monadic. We also discussed that we can define a monadic composition > => in terms of monadic application ( >> = ). Look again at >> = :

( >> = ) :: m b -> ( b -> m c ) -> m c
mv >> = g


where mv is some monadic value of type mb . And after all, too - without any extract , - how does a value of type b come from a value of type mb , in order to pass it to g

The answer for all monads is different. Each monad has its own way of “unpacking” the monadic value in order to transfer it as usual to another monadic function. In other words, the operator >> = is specific for all monads, and in its implementation it is hidden how the monadic value is unpacked and passed on. Also, each monad has its own return statement .

From now on, I will use a slightly different terminology for arbitrary monads. The >> = operator accepts an input monadic value (also known as “action”), “unpacks” it into a normal (non-monadic) value (unpacking for all monads looks different), and then passes this normal value to a monadic function. It already produces a monadic value (“action”) and returns the final result.

Now that we have learned all of this, let's talk about the type class " Monad ". Later we will look at the definition of the operator >> = for some monads, and you will learn how unpacking is done there.

Monad type class


>> = - this is the monadic application operator, and return is the function of converting a normal value to any monadic one. So I said above. However, this terminology is inaccurate, since it was also said that each monad must have its own versions of these operators / functions that are different from the others. On the other hand, it is not at all good to call the operators >> = and return in different ways. We would get unpleasant things like this:

IO >> = :: IO a -> ( a -> IO b ) -> IO b
IOreturn :: a -> IO a

Maybe >> = :: Maybe a -> ( a -> Maybe b ) -> Maybe b
Maybereturn :: a -> Maybe a


What I wrote in Haskell will not pass the syntax check. You cannot mix alphabetic and non-alphabetic characters in identifiers, and you cannot also capitalize the first letter of a function.

The problem is beautifully solved by Haskell's “type classes”. (Remember, I said that the manual would be easier if you are already familiar with type classes? Type classes have nothing to do with classes from OOP, these are completely different things. Haskell type classes look more like compile-time interfaces). A type class is a way of saying that for a heap of different types there are different embodiments of like functions (or operators). So, for example, the class of types Eq contains the operator == with the type a -> a -> Bool (where all a is the same type). For type a to belong to the type class Eq , there must be a suitable operator == for it (comparison for equality). The numeric types Int and Float belong to the class of types Eq (you can also say that they are instances of class Eq ), which means that each of them has its own meaningful operator == . In Haskell, this is written as follows:

class Eq a where
( == ) :: a -> a -> Bool

instance eq int where
( == ) = intEquals - the type of intEquals is: (Int -> Int -> Bool)

instance eq float where
( == ) = floatEquals - the floatEquals type is: (Float -> Float -> Bool)


It is considered that intEquals is a comparison function for Int , and floatEquals is considered to be Float . (I have not yet said about the inequality operator; there is the same idea.) That's all. Now we can use the == operator to compare integers and real numbers with each other. Or to compare any objects, as long as there is a corresponding instance of the Eq type class . And it is very convenient. In the meantime, note that == requires that the elements being compared have the same type; you can't compare, say, Int and Float .

We go further. I repeat that I said: all monads in Haskell are type constructors, and we showed that all monads provide an independent implementation of the >> = operator and return functions. From these two theses we can conclude that there is a type class called (you guessed it) Monad , which was originally defined as:

class Monad m where
( >> = ) :: m a -> ( a -> m b ) -> m b
return :: a -> m a


The Monad type class is not much more complicated than Eq . It is necessary to define only two functions / operator, - not such a big deal, because we already know this. The types of the two functions / operators are the same as those discussed above.

The oddity with the Monad type class is that it is not the same as Eq . Monad is a “class constructor” for which instances (indicated by m ) are not types, but type constructors; we have already seen that all monads must be type constructors. This is how we define an instance of the class constructor (for example, the Maybe monad is taken):

instance Monad Maybe where
( >> = ) = {- version (>> =) for Maybe -}
return = {- the version for Maybe-}


For regular classes and for constructors of classes, the same record is chosen, which may confuse you a little, but a little practice, and everything will fall into place. Perhaps it would be better if Haskell used this notation:

constructorClass Monad m where
( >> = ) :: m a -> ( a -> m b ) -> m b
return :: a -> m a

constructorInstance Monad Maybe where
( >> = ) = {- The Maybe version of (>> =) -}
return = {- the Maybe version of return -}


But it is too detailed. There is usually enough context to understand what is going on.

And now let's analyze a simple example, and then we will talk about the Monad type class.

Example


One of the simplest programs with the IO monad reads text from the terminal and prints it back (with a new line at the end). Naturally, getLine and putStrLn are used for this. Recall their types:

getLine :: IO String
putStrLn :: String -> IO ( )


For them, you cannot use the monadic composition operator > => , because getLine has the form of a monadic value, and not a monadic function. But we can take the operator >> = :

readAndPrintLine = getLine >> = putStrLn


We perform here the (monadic) application of the monadic function putStrLn to the monadic value of getLine . In terms of “actions” that we talked about earlier, we can think of it this way: getLine is an “action” that reads a line of text from a terminal and “returns” its monadic value; operator >> = "unpacks" the entered string from a monadic value, passing it to putStrLn ; putStrLn in turn prints it to the terminal and returns nothing (actually returns empty () as a monadic value).

We could write this explicitly:

readAndPrintLine = getLine >> = ( \ s -> putStrLn s )


Notice that the record (\ s -> putStrLn s) is exactly the same as the simple function putStrLn , by analogy with the record (\ x -> cos x) , which is nothing but cos . So we have not changed anything important here. But it becomes clearer how something (a string of text), when returned from getLine , is passed to putStrLn and printed in the terminal.

We will dwell on this simple example and its variations in the next article in the series, as soon as we learn other functions of the Monad class.

Content

Part 1: The Basics
Part 2: functions >> = and return
Part 3: Monad Laws
Part 4: Maybe Monad and List Monad

Notes

{1} The original is “composability”.
{3} In the original, “Excuse me while I go wash my hands.” Is a practically untranslatable steady expression expressing the author’s confusion, his removal from what was said and self-accusation. Here, I think, the Russian phrase “I wash my hands” would be out of place. “At the beginning of the next paragraph, the sentence“ OK, I'm back. ”Is meant that the author returned after washing his hands. Replaced to maintain meaning.
{4} Another name for anonymous functions: lambda functions.
{5} In the original, “extra points for functional coolness”
{6} In the original - the steady expression "It's all downhill from here". It has an ironic sense that combines two opposites: 1. Climbed to a height, the descent will be easier; 2. It will only get worse.

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


All Articles