Pure functions are functions whose result totally depends on its arguments and it doesnt make any side effecting changes. One of the important property of such functions is given same arguments would give back same results. Lot of languages don’t have restrictions on functions to have that property. But its nice to have that in place since its much easier to reason about functions like that. We have enough tools in mathematics to actually talk about those functions, since all mathematical functions are pure.
IO is one of those computational problem which is hard to model it that way. Most programming languages have a function to get a line of input from the user. How can we we implement such a function in a language which supports only pure functions? Is it even possible to model IO as pure functions?
I would try to model something like that step by step.
Let’s consider the case of a programming language which allows only one line of input to be read, that too at the start of the program, and prints out a result at the end. We can then easily model that by making the main function of the program take a string argument and produce a string. Runtime of such a programming language would read a string, pass it on to the function and prints out whatever the function returns.
And consider this is the starting point of the program, and everything above is done under the hood for you.
IO usually is more interactive than that. Like we take input do some processing, get more input or output and in arbitrary combinations of it. How do we model something like that?
We are going to make use of something we did in last example. Instead of returning the string to output, if we can return some datastructure which lets us know whether to take more input or output. This datastructure would let the runtime exactly know what to do next, in what sequence. Like.,
InputLine takes a function which takes the read input line and produces more
OutputLine outputs whatever value is and calls the function for more
Exit simply exits.
Now our interactive program would look like
Note that none of the functions, even the anonymous functions inside the
IO actions are impure. IO is done in a symbolic way rather than in
And our runtime
Higher order functions
If we look at
OutputLine, the sequencing logic is almost the same. Once we add more io actions like spinning up threads (Yes we can treat them as IO), leaving that responsibility to individual actions doesnt make lot of sense. But thats the only way of composition we provide. In-order to have better ways compose and have proper responsibility segregation we introduce 2 more IO actions.
The use case for
Return becomes explicit when we try to compose couple of more
IO actions. These 2 operations (
Sequence) together form, what is called a Monad. Which is nothing but any structure which has a
map operation. List, Option, Future etc are all Monad.
After some more restructuring of
IO and addition of
map results in our
Note that, we would no longer expose
Sequence to outside world. We only expose
IO interface outside. Hence its safe to have polymorphic behaviours here.And every
IO action should do the same too. Since
exec is impure, as long as
exec isnt directly used by the programs in our language, they remain completely pure.
for syntax is transformed to
flatMaps except for the last one which get converted to
map incase there is an
yield. Now our
SquareNumber program is lot more easier to read, except ofcourse the
_ to eat away the
printLine result. Haskell does it much better, with its
do syntax we can just ignore results.
Also note that the program is still pure, though it might look like it ain’t. Any reasoning we can apply on pure functions can be applied to any part of the program.
Complete source code is here. Individual sections are available as seperate branches.