Lazy assessment and confusion of side effects of IO

This code (taken from Learn You A Haskell ):

main = do putStr "Hey, " putStr "I'm " putStrLn "Andy!" 

obviously desugars to

 main = putStr "Hey, " >>= (\_ -> putStr "I'm " >>= (\_ -> putStrLn "Andy!")) 

Which, as I understand it, can be construed as "To putStrLn" by Andy! "First I need to put the STR" I ", and for this I first need to put the STR" Hi ";

I disagree with this interpretation, which is annoying because the compiler obviously does not, and leaves me confused. The problem I am facing is that lambdas ignore their arguments, shouldn't it be recognized and short-circuited during a lazy evaluation?

In addition, of course, the binding returns an IO action, and when this I / O action falls into the ground state, it is executed. But what to stop him from printing "Hey Andy I Am"? I suspect that everything that connects is doing.

Also, how does an IO action like "IO ()" carry enough information to allow the runtime system "Hey, I'm Andy!"? How is this IO () different from IO () than the prints of "Hello World!" or is it written to a file?

Consider another, from the wikipedia page for the monad:

Sugared version:

 do putStrLn "What is your name?" name <- getLine putStrLn ("Nice to meet you, " ++ name ++ "!") 

Desugared version:

 putStrLn "What is your name?" >>= (\_ -> getLine >>= (\name -> putStrLn ("Nice to meet you, " ++ name ++ "!"))) 

A similar story is here.

I think I just need to see the bind definition for IO, and then everything will be clear. Something else that could help a lot if someone can help me get through how the program really evaluates and identify the exact moments when side effects occur.

+6
source share
4 answers

Read β€œ Clumsy Clumsy Squad ” by Simon Peyton Jones.

See related questions in

Take any such explanation, including mine with salt - no manual marking can replace strict peer-reviewed paper, and explanations necessarily require simplification.

A very rough prospect is that >>= can be thought of as a list constructor:

 data IO = [Primitive] 
Subsystem

and IO deconstructs the value of main and consumes this list. That is, `` main is just a list. So you may want to take a look at the definition of Haskell entry point above is just a list. So you may want to take a look at the definition of Haskell entry point above main , bind` is pretty uninteresting.

You can also read haskell history articles and look at earlier versions of the IO subsystem to understand what is going on.

Also see C-language is a purely functional satirical post by Conal Elliott.

The definition of functional purity is non-trivial, and I remember the document that sets out the definitions, but I do not remember the name.

+10
source

Looking at IO in a real implementation, Haskell is likely to be more confused than it enlightens. But think of IO as how it is defined (assuming you know GADT):

 data IO a where Return a :: IO a Bind :: IO a -> (a -> IO b) -> IO b PutStr :: String -> IO () GetLine :: IO String instance Monad IO where return = Return (>>=) = Bind putStr :: String -> IO () putStr = PutStr getLine :: IO String getLine = GetLine 

Therefore, when you evaluate a program (of type IO () ), all it does is build a data structure of type IO () , which describes how interaction with the world will happen as soon as you execute it. Then you can imagine that the execution mechanism is written, for example, in C, and where all the effects occur.

So,

 main = do putStr "Hey, " putStr "I'm " putStrLn "Andy!" 

coincides with

 main = Bind (PutStr "Hey, ") (\ _ -> Bind (PutStr "I'm ") (\ _ -> PutStr "Andy!")) 

And their sequence comes from how the execution mechanism works.

However, I know that the Haskell implementation does not actually do this. Real implementations, as a rule, implement IO as a state monad with a marker representing the real world (this guarantees consistency), and primitives like putStr are just calls to C functions.

+7
source

I think I just need to see the bind definition for IO, and then everything will be clear.

Yes, you have to do this. It is actually quite simple, and if I remember correctly, it looks like

 newtype IO = IO (RealWorld -> (a, RealWorld)) (IO f) >>= g = ioBind fg where ioBind :: (RealWorld -> (a, RealWorld)) -> (a -> IO b) -> RealWorld -> (b, RealWorld) ioBind fg rw = case f rw of (a, rw@RealWorld ) -> case ga of IO b -> b rw 

The trick is that each IO value is actually a function, but to evaluate it, you need a token like RealWorld . There is only one instance that can provide such a value - the runtime system running main (and, of course, a function that cannot be called).

+3
source

I think this is more understandable if you think again about actions as functions. The binding example ( do { foo <- getLine ; putStrLn foo ; } ) is intuitively similar to the following function:

 apply arg func = func (arg) 

Except that the function is a transaction. Therefore, our call to func(arg) is evaluated if it is only executed if (arg) succeeds. Otherwise, we fail in our action.

This is different from regular functions because then Haskell really doesn't care if (arg) computes fully or in general, until it needs a little func(arg) to continue the program.

+1
source

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


All Articles