The difference between evaluating and doing I / O: what makes Haskell do IO?

What mechanism does Haskell use to actually decide on the 4 actions below?

main :: IO () main = getLine >>= putStrLn >> getLine >>= putStrLn 

Initially, I thought this was due to lazy evaluation, but ... as from Real Word Haskell, about IO actions, they

creates an effect on execution, but is not evaluated

Therefore, I suspect that this is some other mechanism, and not a system that wants to "evaluate" main . What is this mechanism? Or, if it is an assessment that Haskell "wants" to evaluate, what causes it to carry out a chain of actions?

+5
source share
3 answers

As a first-order approximation, the main source of evaluation in Haskell is main . It means that:

  • IO actions can be compiled and compiled via >>= , >> , <*> , fmap , etc. to create any other IO actions, but
  • the main IO action will only create effects.

In a sense, everything Haskell does is done by main :: IO () . In order to be appreciated, it must stand in the way of performing an IO action (this is where laziness fits). This begs the question: what does it mean to actually launch an IO action?

Under the hood, the IO ends up behaving like a (strict) State monad, which transfers the RealWorld state through it (which does not contain information - it is a symbolic state that side effects cover in the world), so the โ€œrunningโ€ IO (like the equivalent of State RealWorld ) is similar to calling runState . Naturally, this runState can only appear once for any program - and this is exactly what main does (and what makes it magical)!

+12
source

This may seem odd, but doing IOs actually goes beyond the usual Haskell language! 1

Haskell built-in libraries provide โ€œbasicโ€ I / O, such as getLine :: IO String , functions that return I / O, such as putStrLn :: String -> IO () , and ways to create I / O from other operations I / O (mainly by providing the Monad interface, therefore everything that works on any monad, like all things in Control.Monad , is a way to work with IO ).

All this is clean and lazy, just like the non-IO Haskell code. IO not a special case for everything you can do with regular Haskell code (so you can use the general Monad code in IO; all this code is written and compiled without any knowledge of any special rules that IO has, so it can only work if they are not).

But none of this actually ever performs an IO action; it just makes new IO actions from others. This is what people mean when they talk about how "evaluating the performance of an EUT does not work." The value of "apple" ++ "banana" type String can be represented by an invaluable thunk; when it gets the "applebanana" score, it still represents the exact same value, the system just writes it as data in memory, and not a pointer to some code that you can run to create it 1 . Similarly, a putStrLn "apple" >> putStrLn "banana" value putStrLn "apple" >> putStrLn "banana" type IO () can be represented by an invaluable thunk, and when it gets evaluated, all this means that the system now represents the same value with a data structure instead of a pointer to a code that will run the (clean, lazy) function >> for the other two I / O operations. But we talked only about the systemic representation of I / O operations in RAM, nothing about actually launching them to create some side effects.

And actually Haskell language features that talk about IO actions are missing. The runtime system "just knows" how to execute the IO main action from main 3 module. Haskell cannot talk about how this happens; that everything is handled by a system that provides you with Haskell (GHC or another Haskell system). The only option the Haskell language gives you is that main is defined as a Haskell action; any I / O actions that you include as part of the main definition will be triggered.


1 I pretend that things like unsafePerformIO do not exist for the purpose of this discussion. As the name implies, it intentionally violates the usual rules. It is also not intended to introduce "performing I / O operations" as a normal part of the Haskell language, but only to use in the internal details something that is a "normal Haskell interface".

2 Usually this happens in part: only very simple types, such as Int , are evaluated as "all or nothing." Most of them can be partially evaluated in data structures that contain thunks deeper (which may or may not be evaluated later).

3 Or GHCi "just knows" how to perform the I / O actions that you enter at your prompt.

+6
source

According to https://wiki.haskell.org/IO_inside#Welcome_to_the_RealWorld.2C_baby , there is a โ€œfakeโ€ type that represents the real world, RealWorld and IO (a) actually a function.

 type IO a = RealWorld -> (a, RealWorld) 

So, main , as you would expect in other languages, is actually a function

 main :: RealWorld -> ((), RealWorld) 

which is called when the program starts. Therefore, to evaluate the final result, which is of type ((), RealWorld) , Haskell must get the value of the RealWorld component, and for this it must run the main function. Note: this is a runtime that runs this function. There is no way in Haskell to start executing this function.

When

 main = getLine >>= putStrLn >> getLine >>= putStrLn 

each of the actions is actually functions, and in order to work out the RealWorld value displayed at the end of the final putStrLn , it will need to run it and all the actions leading to it.

So this is a lazy assessment, but the hidden meaning of RealWorld .

0
source

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


All Articles