📜 ⬆️ ⬇️

Haskell without monads

Any programmer who studies haskell, sooner or later meets with such an incomprehensible concept as a monad . For many, familiarity with the language ends monad. There are many monad guides, and new ones are constantly appearing (1). The few who understand monads carefully hide their knowledge, explaining monads in terms of endofunctors and natural transformations (2). No experienced programmer can find a place for monads in his established picture of the world.

As a result, java-programmers only laugh at Haskel, not looking up from their million-line enterprise project. C ++ developers patch their super-fast applications and come up with even smarter pointers. Web developers scroll through examples and huge specifications for css, xml and javascript. And those who study haskell in their free time face a formidable obstacle, the name of which is monad.

So, we will learn how to program in haskel without monads .

')
To do this, we need some free time, a good night’s sleep, a mug of a favorite drink and the ghc compiler. In windows and macos, it can be found in the haskell platform (3) package; linux users can install ghc from the repository. Code samples starting with Prelude> can be checked in ghci, an interactive interpreter.

On Habré there was already a similar article (4), however, it does not explain all the ins and outs of I / O, but simply offers ready-made templates for use.

Transition to the next action - operator


Let's start from afar. All calculations in Haskel were divided into “with side effects” and “without side effects”. The first is, for example, writing / reading from I / O devices, calculations with the possibility of an error somewhere in the middle, etc. The “no side effects” include such operations as adding numbers, sticking together strings, any mathematical calculations — any “pure” functions.

Pure functions are combined in the same way as functions in all other programming languages ​​are combined:
Prelude> show (head (show ((1 + 1) -2))) '0' 


For the compilation of programs with side effects, a special operator was created
 >>= 

let's call it "connect" (eng. bind). All I / O actions are glued to them:
 Prelude> getLine >>= putStrLn asdf asdf 

This operator accepts 2 functions with side effects at the input, and the output of the left function supplies the input with the right one.

Let's look at the types of functions with the interpreter command: t:
 Prelude> :t getLine getLine :: IO String Prelude> :t putStrLn putStrLn :: String -> IO () 


So, getLine takes nothing as input, and returns the type IO String.

The fact that the name type 2 words, says that this type of compound. And the word that comes first is called the type builder, and everything else is the parameters of this builder (I know what sounds like an incongruous sound, but that’s necessary).

In this case, the word IO just indicates a side effect, and it is discarded by the operator ”=. As an example of other “indicators” of side effects, we can cite the popular type State, which means that the function has some kind of state.

Let's go to putStrLn. The function accepts a string as input, and returns IO (). With IO, everything is clear, a side effect, and () is a Haskel-like analog of a void. Those. the function does something there with I / O and returns an empty value. By the way, all programs on HASKEL must end with this same IO ().

So, the "connect" operator takes its result from the first argument, cuts off the side effect indicator and transfers what came to the second its argument. It seems complicated, but on this one operator, half of the Haskel is kept, all the I / O is programmed with it. It is so significant that it was even added to the language logo.

What if the return and accept values ​​of the functions to be glued do not match? Lambda functions come to the rescue. For example, we just take a parameter as input, but do nothing with it:
 Prelude> (putStrLn " 1") >>= (\a -> putStrLn " 2") >>= (\b -> putStrLn " 3")  1  2  3 

Looking ahead, I will say that the operator "= has a very low priority and, if you wish, you can do without brackets in this example. In addition, if the argument is not used inside the lambda function, as in our example, you can replace it with _.

Let's rewrite the first example to be completely equivalent, but using the lambda function:
 Prelude> getLine >>= \a -> putStrLn a asdf asdf 

When displaying the string on the screen, we now clearly indicated with the help of the lambda function that we accept one variable, and explicitly wrote how we use it.

Did you say "variable"?


Yes, now let's talk about variables. As you know, in Haskel there are no variables. However, if you look at any listing, you will see a lot of assignments.

In the code above, a and b are very similar to variables. They can also be referenced as in other languages. However, these a and b differ significantly from variables in imperative languages.

In all imperative programming languages, a variable is a named memory region. In Haskell, things like a and b are named expressions and values.

We give an example and show these differences. Consider the following code on C:
 a = 1; a = a + 1; printf("%d",a) 

Everything is crystal clear and the result is predictable.

Now do the same on haskel:
 Prelude> let a = 1 Prelude> let a = a + 1 Prelude> print a ^CInterrupted. 

Code execution will never end. In the first line, we define a as 1. In the second line, we define a as a + 1. By the time the second line is read, the interpreter forgets the previous value of a, and determines a again, in this case, through itself. Well, this recursive definition will never be calculated.

As for the named memory areas, they are in Haskel, but this is a completely different story.

With this design, you can pass parameters through several calls of the "connect" operator:
 Prelude> getLine >>= \a -> putStrLn " :" >>= \_ -> putStrLn a asdf  : asdf 


Real code


Now, using our secret knowledge, we will write something real. Specifically, a program that receives data from the user, performs some actions on them and displays the result on the screen. We will write the program as it should be in a separate file and compile it into machine code.

Let's call the file test.hs:
 main = putStrLn "  :" >>= \_ ->       getLine >>= \a ->       putStrLn "   :" >>= \_ ->       putStrLn (show ((read a)^2)) 

compile:
 ghc --make test.hs 

run:
 $ ./test   : 12    : 144 

The read function attempts to parse a string into a value of the desired type. Which type she herself guesses is a separate story. The show function converts a value of any type to a string.

The read function is not safe if we give it letters and ask to parse the number, an error will occur. We will not dwell on this, just mention that there is a safe module for this case.

Admixture of purity


Separately, the question arises of how to call pure functions from a side-impact code.

In the example above, the net function is simply written as an argument to the IO function. Often this is enough, but not always.

There are other ways to call clean code.

The first of these is the forcible transformation of a clean code into a side-effect code. Indeed, pure code can be considered a special case of a side-effect, therefore such a transformation does not pose any danger. And it is carried out using the function return:
 main = putStrLn "  :" >>= \_ ->       getLine >>= \a ->       putStrLn "   :" >>= \_ ->       return (show ((read a)^2)) >>= \b ->       putStrLn b 

We compile, check, the program works as before.

Another way is to use the haskalla let ... in ... In many manuals, enough attention is paid to it, so we will not dwell on it, I will give only a ready-made example:
 main = putStrLn "  :" >>= \_ ->       getLine >>= \a ->       putStrLn "   :" >>= \_ ->       let b = (show ((read a)^2)) in       putStrLn b 


Need more sugar


The developers of the language have noticed that often there are constructions
 >>= \_ -> 

therefore, to designate them, we introduced the operator
 >> 

Rewrite our code:
 main = putStrLn "  :" >>       getLine >>= \a ->       putStrLn "   :" >>       let b = (show ((read a)^2)) in       putStrLn b 

So it became a little more beautiful.

But there is a cooler feature - “do” syntactic sugar:
 main = do    putStrLn "  :"    a <- getLine    putStrLn "   :"    let b = (show ((read a)^2))    putStrLn b 

Exactly what is needed! So you can already live.

Inside the do block, bounded by left justification, the following replacements occur:
 a <- abc   abc >>= \a -> abc   abc >> let a = b   let a = b in do 

The “do” notation makes the syntax very similar to the syntax of all modern programming languages. Nevertheless, under the hood, she has a rather well thought-out mechanism for separating a clean and side-effect code.

An interesting difference is the use of the return statement. It can be inserted in the middle of the block, and it will not interrupt the execution of the function, which can cause confusion. But in reality, it is often used at the end of a block to return a pure value from the IO function:
 get2LinesAndConcat:: IO String get2LinesAndConcat = do    a <- getLine    b <- getLine    return (a + b) 


Sphere in vacuum


And now we will carry out our pure code in separate function. And at the same time we will place, finally, the missing type signatures.
 main :: IO () main = do    putStrLn "  :"    a <- getLine    putStrLn "   :"    let b = processValue (read a)    putStrLn (show b) processValue :: Integer -> Integer processValue a = a ^ 2 

The important point is that the side-effect I / O code can only be run from the I / O code. However, clean code can run from anywhere.

Thus, the pure functional world is strictly and reliably separated from all that is associated with side effects. Inside the processValue we can count anything, implement any logic. But even if a million lines of code is called from there, we can be sure that for any input value, the output will always be the same. And the parameter passed there is surely not spoiled by anyone, you can safely use it further.

In stylistic guides, it is recommended to minimize the use of side-effect code and make the maximum of functionality into pure functions (5). However, if the program is designed to perform I / O actions, you should not avoid using it wherever necessary. As a rule, in such cases, auxiliary functions are required, which can be clean. Experienced programmers in Haskel recognize the excellent support even of IO code in comparison with imperative languages ​​(the statement is attributed to Simon Peyton Johnes, but a direct link was not found).

One feature of performance is associated with clean functions. Let's take a classic example, transfer to a function a complex structure “employee” with many fields. So, by analogy with C, the efficiency of the code will be comparable to passing this parameter by pointer, and reliability is comparable to passing a parameter through the stack, because in C, only passing through the stack guarantees the immunity of the original structure.

What are you carrying?


“This code is terrible, it is unnecessarily complex, has too little in common with the warm lamp semantics of all other languages, for any purpose c / c ++ / c # / java / python etc. is enough.”

Well, there is some truth in this. Here you need to decide what you think is terrible: the separation of side effects from clean code or a specific implementation of this mechanism.

If you know how to make such a mechanism more simple and understandable, please tell the world community about it! Haskelny community is very open and friendly. In the draft of the new standard, which is adopted regularly, any proposals are considered, and if they really are worth, they will be accepted.

If you think that “in python everything is good that you are attached with your side effects!”, No one bothers you to use the tool that you like. From myself I can add that Haskel really simplifies development and makes the code more understandable. The only way to make sure of this or the opposite is to try writing in Haskel!

Where to go next


For further study, or instead of this article, we can recommend the article “a soft introduction to haskell” (6), and especially its translation (7).

In addition, of course, any other articles (8) will do. There are a lot of manuals, but they all explain the same things from different points of view. Unfortunately, very little information has been translated into Russian. Despite the abundance of manuals, the language is simple, its description together with the description of standard libraries takes only 270 pages (9).

A lot of information is also contained in the documentation for standard libraries (10).

I would be glad if the article will help someone or just seem interesting, comments and criticism are welcome.

ps What I called the “type builder” in the Haskel world is called the “ type constructor ”. This is done to make it easier to forget the meaning of the word "constructor", taken from the PLO, these are completely different things. The situation is aggravated by the fact that, in addition to type constructors, there are also data constructors that also have nothing in common with OOP.

Links


  1. www.haskell.org/haskellwiki/Monad_tutorials_timeline
  2. http://en.wikipedia.org/wiki/Monad_(category_theory)
  3. hackage.haskell.org/platform
  4. habrahabr.ru/blogs/Haskell/80396
  5. www.haskell.org/haskellwiki/Avoiding_IO
  6. www.haskell.org/tutorial
  7. www.rsdn.ru/article/haskell/haskell_part1.xml
  8. www.haskell.org/haskellwiki/Tutorials
  9. www.haskell.org/definition/haskell98-report.pdf
  10. www.haskell.org/ghc/docs/7.0.3/html/libraries


upd: (SPOILER!)

As I was correctly suggested in the comments, the choice of the name for the manual on monads is not entirely successful. Since the topic of monads is not disclosed, there is a sense of understatement.

So, the word "monad" is called a set of operators
 >>= >> return fail 

and any data type on which they are defined. For example, IO.

There was a not very good aura around this word, but in reality there is no secret meaning in it. This is simply the name of a programming pattern that can be explained without monads .

upd2:
User afiskon provided a link to an interesting presentation.
about haskel .

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


All Articles