- What are monads for?
- To separate clean calculations from side effects.
(from online discussions about Haskell)
Sherlock Holmes and Dr. Watson are flying in a balloon. Fall into thick fog and lose orientation. There is a small gap - and they see a person on earth.
- Dear, would you tell me where we are?
- In the balloon basket, sir.
Then they are carried further and again they see nothing.
“It was a mathematician,” says Holmes.
- But why?
- His answer is completely accurate, but absolutely useless.
(joke)
First, they need to understand that the introduction of an additional level of complexity in the form of the proposed abstraction eliminates a much greater level of complexity with which they constantly encounter. Therefore, I will describe the huge number of problems that programmers will no longer face using pure functions (do not worry, I will explain below what it is) and the described abstractions.
Secondly, people need to understand how the proposed abstraction was implemented, and how this implementation allows it to be used not in a particular case, but in a huge variety of different situations. Therefore, I will describe to you the logic behind the implementation of Haskell language abstractions, and show that they are not only applicable, but also provide significant advantages in an incredible number of situations.
And, thirdly, people need to understand not only how abstractions are implemented, but how to apply them in their daily lives. Therefore, I will describe in the article and this. Moreover, it is not easy, but very simple - even easier than to understand how these abstractions are implemented (and you yourself will see that it is quite easy to understand the implementation of the described abstractions).
addThreeNumbers xyz = x + y + z
addThreeNumbers xyz
we can substitute the expression x + y + z
, and we get the same result. The compiler, by the way, does just that - when it encounters a function name, it substitutes an expression defined in its body instead.The result of a function depends only on its arguments. No matter how many times we call this function with the same arguments, it will always return the same result to us, because the function does not refer to any external state. She is completely isolated from the outside world and takes into account only what we explicitly passed on to her as her arguments. Unlike such a science as history, the result of mathematical calculations does not depend on whether the Communists are in power, the Democrats or Putin. Our function comes from mathematics - it depends only on the arguments passed to it and nothing more.
You can check it yourself: no matter how many times you pass this function 1, 2 or 4 as arguments, you will always get 7 as a result. You can even "(2 +1)" instead of "3", and instead "4" - "(2 * 2)". There is no other way to get other results with these arguments.
TheaddThreeNumbers
function isaddThreeNumbers
called pure because it is not only independent of the external state, but also not capable of changing it. It cannot even change the local variables passed to it as arguments. All she can (and should) do is calculate the result based on the values of the arguments passed to her. In other words, this feature has no side effects.
Since the result of calculating pure functions does not depend on the external state and does not change the external state, we can calculate such functions in parallel, without worrying about data race , which compete with each other for common resources. Side effects are the destruction of parallel computing, and since our pure functions do not have them, we have nothing to worry about. We simply write pure functions, not caring about the order in which the functions are calculated, or how to parallelize the calculations. Paralleling we get out of the box, simply because we write in Haskell.
In addition, since calling a pure function several times with the same arguments, we are always guaranteed to get the same result, Haskell remembers the result that was calculated once, and doesn’t calculate it again when calling a function with the same arguments, but instead substitutes the previously calculated . This is called memoization . It is a very powerful optimization tool. Why count again if we know that the result will always be the same?
g :: a -> b
(read as “a function g that takes an argument of type a and returns values of type b”) and a function f :: b -> c
, then we can compose it to get a function h :: a -> c
. Applying the value of type a to the input of the function g, we obtain the value of the type b at the output - and the values of this type are taken by the function f. Therefore, we can immediately transfer the result of calculating the function g to the function f, the result of which will be a value of type c. This is written like this: h :: a -> c h = f . g
(.) :: (b -> c) -> (a -> b) -> (a -> c)
b -> c
as the first argument (the arrow also indicates the type - the type of the function), which corresponds to our function f. With the second argument, it also accepts a function - but already with the type a -> b
, which corresponds to our function g. And it returns the composition operator a new function — with the type a -> c
, which corresponds to our function h :: a -> c
. Since the functional arrow has the right associativity, we can omit the last parentheses: (.) :: (b -> c) -> (a -> b) -> a -> c
b -> c
and a -> b
, as well as an argument of type a, which is passed to the input of the second function, and at the output we get a value of type c
, which the first one returns function.f ∘ g
used to denote a composition of functions, which means “f after g”. The point is similar to this symbol, and therefore it was chosen as the composition operator.f . g
f . g
means the same as f (gx)
- i.e. the function f
applied to the result of applying the function g
to the argument x
.A marketer asks a programmer:
- What is the difficulty of supporting a large project?
- Well, imagine that you are a writer, and support the project "War and Peace", - the programmer answers. - You have TK - write a chapter about how Natasha Rostova walked in the rain in the park. You write “it was raining”, you remain - and the error message flies to you: “Natasha Rostova died, the continuation is impossible.” How did she die, why did she die? You start to understand. It turns out that Pierre Bezukhov has slippery shoes, he fell, his gun hit the ground, and the bullet from the post ricocheted into Natasha. What to do? To charge the gun with idle? Change shoes? We decided to remove the post. Removed, save and get the message: "Lieutenant Rzhevsky died." Again you sit down, understand, and it turns out that in the next chapter he leans on a pole that is no longer there ...
How in this very spherical horse in vacuum to get the initial data for our programs, which come just from the outside world, from which we isolated? You can, of course, use the result of user input as an argument to our clean function (for example, thegetChar
function that accepts character input from the keyboard), but, first, in this way we will let in our cozy clean world a “dirty” function that we it will break there, and, secondly, such a function will always have the same argument (thegetChar
function), but the calculated value will always be different, because the user (this is an ambush!) will always press different keys.
How to produce the result in the external world isolated by us from our cozy purely functional world, the result of the program? After all, a function in the mathematical sense of this word must always return a result, and functions that send some data to the outside world do not return anything to us, and therefore they are not functions!
What to do with the so-called partially defined functions - that is, with functions that are not defined for all arguments? For example, the well-known division function is not defined for division by zero. Such functions are also not full-fledged functions in the mathematical sense of the term. You can, of course, throw an exception for such arguments, but ...
... but what do we do with the exceptions? Exceptions are not the result that we expect from pure functions!
And what to do with non-deterministic calculations? That is, with those where the correct result of the calculations is not one, but many of them. For example, we want to get a translation of a word, and the program gives us several of its meanings at once, each of which is the correct result. A pure function should always produce only one result.
And what to do with the continuations? Continuation is when we make some calculations, and then, without waiting for them to finish, we save the current state and switch to perform some other task, so that after its execution we return to the incomplete calculations and continue from where we left off. What state are we talking about in our purely functional world, where there is no state and cannot be?
And what, finally, should we do when we need not only to somehow consider the external state, but also to somehow change it?
Sometimes we have functions that are not defined for all arguments. When we pass this function the arguments on which the function is defined, we want it to calculate the result. But when passing its arguments on which it is not defined, we want the function to return something else to us (exception, error message, or an analogue of imperative null
).
Sometimes functions can give us not one result, but something else (for example, a whole list of results, or no result at all (an empty list of results)).
Sometimes, to calculate the value of a function, we want to receive not only arguments, but also something else (for example, some data from an external environment, or some settings from a configuration file).
Sometimes we want not only to get the result of the calculation to pass the next function, but also to apply it as an argument to something else (getting some state to which we can then return to continue the calculation, which is the meaning of continuations).
Sometimes we want not only to make calculations, but also to do something else (for example, write something to the log).
Sometimes, by compositing functions, we want to transfer to the next function not only the result of our calculation, but also something else (for example, some state that we first considered from somewhere, and then somehow changed in a controlled manner).
( / - ) { // / // - return ( / - ) }
Double
. As a result of the function that counts in inches, the arguments expressed in meters were passed, which naturally led to an error in the calculations. data DistanceInMeters = Meter Double data DistanceInInches = Inch Double
DistanceInMeters
and DistanceInInches
are called type constructors, and Meter
and Inch
are data constructors (type constructors and data constructors inhabit different scopes, so they could be made the same).Double
and returning a result of calculating a value of type DistanceInMeters
or DistanceInInches
? So it is - the data constructors are also functions! And if earlier we could accidentally pass to a function that takes a Double
, any value that has a Double
, then now in this function we can specify that its argument should contain not only the value of the Double
type, but also something else , namely «» Meter
Inch
.Meter
Inch
Double
. , — «- » — , «», «- », . Haskell. Haskell: data Maybe a = Nothing | Just a
Maybe
, a
. « » , — Double
, Bool
, DistanceInMeters
, . , Maybe a
2 — Nothing
Just
( a
). «»: Nothing
, Just
- (, Just True
) — Maybe a
( Just
True
, Maybe Bool
).Maybe
, . - ( Just
), ( Nothing
). , , - Maybe
, . : , , — .Maybe
Haskell — , . , lookup
, (, ), , . , . Nothing
, — , Just
. Those. , , ( Just
), , — «- » ( Nothing
).Nothing
, , «- » ? : , , , — , . , : data Either ab = Left a | Right b
Either
takes 2 type variables - a
and b
(which can be different types, but can be of the same type - as we like). If the result of the calculations was successful, we get them in a wrapper Right
(the result of the calculations will be of type b
), and if the calculations failed, then we get an error message of type a in the wrapper of the data designer Left
. data Reader ea = Reader (e -> a)
e
(, ), a
. e -> a
, .. .[a]
( : [] a
, []
«- », a
— )., , , .
, , , «- ». , , «- », «- ».
«- », . «- » .
«- » «» , «» .
:
a -> mb
m
— « »,b
.
a -> b
, .. .ma
.ma -> mb
, , «»a -> b
m
,a -> b
,mb
.
, , first class citizens. Those. , — , .. , , , «»m
.f
,a
, ,mf
ma
, « »:
mf ` ` ma => m (f ` ` a).
, , , , , , .f :: b -> c
g :: a -> b
,f . g
,g
,f
.f :: b -> mc
g :: a -> mb
?mb
b
— , ,b
«»m
.
«»b
m
, . «» «- », . , ,a -> mb
b -> mc
,a -> mc
, , «- ». , , , , .
, , , .
, , , .
, , — , , , «- », .
isChar :: a -> Bool
, , Char
, , . , Maybe a
2 — Just
Nothing
: maybeIsChar :: Maybe Char -> Maybe Char -> Maybe Bool maybeIsChar (Just x) = Just (isChar x) maybeIsChar Nothing = Nothing
fmap
: fmap :: (a -> b) -> ma -> mb
fmap
. fmap
Maybe a
: fmap f (Just x) = Just (fx) fmap _ Nothing = Nothing
fmap
a -> b
. , , , , . - , . , .Maybe a
a -> b
. , fmap
. -: Maybe a
, .fmap
, «» , , . , — ! , .. , .<*>
( apply; , , , , ; , , ): (<*>) :: m (a -> b) -> ma -> mb
Maybe a
. , 2 , , , , ( Just
), Nothing
: (Just f) <*> Nothing = Nothing Nothing <*> _ = Nothing (Just f) <*> (Just x) = Just (fx)
Maybe
. , , — (<*>)
. , , , (<*>)
pure
.pure
? , , ! pure
. : pure :: a -> ma
pure
Maybe
, : pure x = Just x
Just 2
Just 3
: pure (+) <*> Just 2 <*> Just 3 > Just 5
Maybe
(+)
, . 2 (<*>)
.liftAN
, A Applicative (functor), N , , . (+), : liftA2 (+) (Just 3) (Just 2) > Just 5
( | a + b | )
( | (Just 3) + (Just 2) | ) > Just 5
fmap
. , , , , .pure
<*>
- and this also allowed us to apply ordinary functions to the wrapped values, taking any number of arguments. And as soon as we defined these functions for the wrapper type, he immediately earned the right to be called an applicative functor. By the way, in order to make a wrapper type an applicative functor, you must first make it an ordinary functor (and it’s not possible to cheat - the compiler will follow this). This is a logical (and, as usual, simple) explanation, which I will leave to you for self-study, because the article has already become swollen.a -> mb
andb -> mc
, , «- », . , , , . , , , .return
. return
, . return
, : return :: a -> ma
pure
, ? , . , , , ( — ), , pure, return
: return = pure
(>>=)
( bind). : (>>=) :: mb -> (b -> mc) -> mc
(>>=)
a -> mb
, , , -, «- » ( «- », ), . Those. a -> mb
, mb
, , - . , , (>>=)
. .(>>=)
Maybe
. 2 , 2 . , b -> mc
k
, « » ( , , , « », return
« »): — Nothing, Nothing Nothing >>= _ = Nothing — , "" k (Just x) >>= k = kx
Maybe
— ! , — return
(>>=)
.Maybe
Just
, Nothing
. , - Nothing
, . ? if then else
, Nothing
?if then else
. (>>=)
, . : - Nothing
, (>>=)
«» , . , Nothing
. , .Either ab
, which allows us to more clearly work with errors and exceptions than the type Maybe
. Let's recall the definition of this type: data Either ab = Left a | Right b
Left
— a
— , , «- », — Right
— b
— «» . «» , , Right
. — , Left
.return
: return x = Right x
return
, - b
, -_ Right
Either ab
.(>>=)
. , Maybe
: , Either ab
, , -_ Left
— . (.. , -_ Right
), : (Left x) >>= _ = Left x (Right x) >>= k = kx
Maybe
Either
. 2 , (, , , «» ). , .a -> [b]
. (>>=)
ma
— , [a]
( a
). , a
.fmap
( , , — ?). fmap
, m
: fmap :: (a -> b) -> [a] -> [b]
fmap
, : fmap :: (a -> [b]) -> [a] -> [[b]]
fmap
mb
, mmb
, .. . (>>=)
, «». concat
, , , . (>>=)
: [] >>= _ = [] xs >>= k = (concat . fmap k) xs
(>>=)
is the same in all cases. In each wrapped value, we have the result of calculations and “something else”, and we think that we need to do it with calculations and with this “something else” when transferring to another function. “Something else” can be a marker of successful or unsuccessful calculations, it can be a marker of successful calculations or an error message, it can be a marker that our calculations can return from zero to infinity of results. “Something else” can be a log entry, a state that we read and pass as an argument for our “basic” calculations. Or the state that we read, change and transfer to another function, where it changes again - in parallel with our “basic” calculations.return
(>>=)
.(>>=)
, , . , Haskell, , . . -, Haskell , , . -, ( ) , , .(>>=)
. , - (>>=)
, , , , .(>>=)
— , . Haskell, (>=>)
, «» («fish operator»): (>=>) :: (a -> mb) -> (b -> mc) -> a -> mc (f >=> g) x = fx >>= g
x
we have is type a
, f
and g
is Kleisley's arrows. Applying the Klaisley arrow f
to the value x
, we get the wrapped value. And how to transfer the wrapped value to the next Klaisley arrow, as you remember, the operator knows (>>=)
.Writer
(«- » ), , . Haskell « », , , , , , , . -, (, , , ).Source: https://habr.com/ru/post/272115/
All Articles