This is part 6 of a 6 part tutorial
The IO Monad
In pure functional programming, all functions are supposed to be referentially transparent, and that means that each time you call a function with the same arguments, it should give you the exact same result. When functions are referentially transparent, you have a lot less worries about whether or not it will always work correctly.
A mathematical function is never going to give you a different answer no matter how many times you give it the same argument. The reason for that is pretty much that it cannot get any values from anywhere other than what you passed it, so it can never be any different.
In an imperative programming language you could write a sin(x)
function which
was completely evil and called time()
, getting a value from somewhere besides
the x
parameter. If the time in seconds was even, it would add 1 to the result
it returns, and if not it wouldn’t.
This example is just plain evil, especially if every time you happen to test
the sin()
function it happened to be an odd time in seconds, until one
important day a million astronauts burn to death in the depths of space because
it was run on an even second. Silly example but that is the nature of many
bugs in the imperative programming world.
All of these problems involve IO. If you say no functions can do any input or output to the OS, then the problem is solved, except you can also never interact with the program in any way.
The answer is to let some functions do IO, but do it inside a container called
the IO Monad from which you aren’t supposed to be able to escape. The reason
you aren’t able to escape, is because the data constructor for IO is hidden
from use, by hiding it in the IO Module. This means the type signature for
every function which does IO will be something like main :: IO ()
.
Any function which calls another function that does IO, getLine :: IO String
,
for example, must also return something wrapped in IO. It can’t deconstruct
the return value from getLine
into just a String
using the IO data
constructor and return that. It can pass the pure string to a pure function
though, by using bind.
Here is an example of doing IO to get a number to pass to the pure function sin.
Doing some IO in the IO Monad
1 2 3 4 5 |
|
This looks like imperative code, telling you which order to do things and
sharing the results of subsequent function calls. getLine
returns a type IO String
,
remember this is like Container String
from part 1.
The function putStrLn
always returns IO ()
, read IO null, and since it is the
last thing, that is returned from the entire function, as you would expect.
In reality, it is converted to this:
Explicitly using bind
1 2 3 4 5 |
|
This is really one long expression, and not a recipe as it looks in do
notation. Since haskell is lazy, it probably does not do any computation until
it reaches the putStrLn
function, which I think is strict (evaluates it as soon as it sees it).
When putStrLn
evaluates its argument it finds valueStr
and finds that it
does’t have the value worked out yet. It sees that it comes passed in through
the lambda and that forces it to call getLine
, and the use enters their text.
Then it evaluates the let statement to find the result, has a complete
string, prints it out, and putStrln
returns IO () from the lambda, and
according to the definition of bind, also returns IO ()
from the bind
expression, and then the function getSin2
itself.