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:
- take an initial value of type a ;
- apply the function f to it (the usual application of functions) and get the result of type IO b ;
- take a value of type IO b from function f and extract the value of type b (this is what we cannot do);
- 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.
- There are two fundamental monadic operations: “bind” (operator >> = ) and return .
- Bind operator is a monadic application operator. Through it, you can specify a monadic composition operator, which looks like this: > => .
- The return statement converts normal values ​​to monadic ones. With its help, the function of transforming ordinary functions into monadic functions is determined.
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
gThe 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 BasicsPart 2: functions >> = and returnPart 3: Monad LawsPart 4: Maybe Monad and List MonadNotes
{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.