Input/Output and Side Effects
Learning Objectives
- You know how pure functional programming languages can still have side effects like reading input and printing output.
- You know of I/O in .
- You are familiar with the
io.pureconstructor andio.bindoperator. - You can write simple programs involving multiple I/O operations.
You will need a local installation to try out the examples in this chapter. Go to πππβ++ and Tooling for instructions on how to install on your machine.
Programs
As is a relatively pure programming language, you might think that itβs not much more than a calculator with type checking. A valid argument for this would be to say that because functions are mathematical functions (or at least functional relations) they cannot doing anything βusefulβ. So how can one then write computer programs in or any pure functional programming language for that matter? This is what we will cover in this chapter.
Input and Output
Unlike functions in mathematics, programs (or any executable code in general) tend to interact with their environment. Most interactions can usually be categorized as input or output interactions:
- Input interactions include, for example, the program reading from a file or standard input.
- An output interaction can likewise be writing to a file or standard output.
This is where execution steps in. Instead of just evaluating terms, execution executes I/O actions that are described by those terms. With I/O actions, is capable of I/O side effects β but only when those actions are executed. I/O actions are pretty much just values and evaluating them is still purely functional β nothing outside what mathematical calculations can do.
I/O actions are βmathematical descriptionsβ of what the computer should do next. Like functions in functional languages, I/O is also a βfirst-class citizenβ β you can take I/O as input, put I/O in a list or even put polymorphic types inside I/O. The key high-level properties of I/O are as follows:
- being able to also perform pure calculations with impure inputs, and
- being composable into larger I/O actions that do multiple actions.
These are in fact the properties of so-called monads1β¦
Many purely functional programming languages support I/O actions.
To demonstrate, we will look at two code examples: one with a pure main function, and another with also a pure main function, but which returns an I/O action.
The latter is merely evaluated in , and the resulting value is a print-value of type IO Unit.
Before going into reading user input, letβs talk a bit more about side effects.
What Are Side Effects?
Side effects in computer science are commonly defined as βobservable changeβ in the βexternal environmentβ when running a program. A program which doesnβt have side effects is called βpureβ regardless of the programming language. A programming language where all valid programs are pure is called a pure programming language.
Observable change means that the state of the operating system or peripherals changed during execution of a program. The word βobservableβ here carries quite a lot of responsibility and is quite tricky to pin-point. For example, evaluating a term to a value does not have side effects as long as evaluation is doing only a mathematical calculation. This means that the state of the CPU and the programβs memory are not considered as part of the external environment.
Printing or tracing, on the other hand, are considered observable changes to the external environment. They arenβt just mathematical calculations (unless you somehow model the whole operating system mathematically), and they can affect the operating systemβs state beyond the program itself.
However, unlike reading input, printing does not affect the state of the program, so functionality is preserved. Printing can be defined as a no-operation in the evaluation rules, and the implementation then steps outside the specification by printing a value instead of doing nothing.
Another interesting example of impurity is an infinite loop. Itβs impossible to go into an infinite loop when doing a mathematical calculation, as all mathematical functions are total.
Like trace, this is another common source of impurity in common so-called pure programming languages such as Haskell or even .
Most pure programming languages still do support reading input, e.g. via a separate type: IO.
I/O Effects in
To learn more about
IO, check Introduction to IO on HaskellWiki.
IO, like List, is a type constructor in .
So just like List Int is not an Int, IO Int is also not an Intβit is a value that represents an input/output computation which will eventually produce an Int.
The type IO T represents the I/O actions that can be executed to produce a value of type T, but it cannot be done during normal evaluation.
Pure code is not allowed to βlook insideβ an IO and extract its result.
Only a function whose return value is IO T is allowed to execute I/O effects and use the results to calculate.
Itβs important to notice that IO T doesnβt have to do a real I/O action, it can also just wrap a pure value of type T.
This is where one of the constructors of IO called pure gets its name β pure 5 : IO Int always produces 5 without doing any actual I/O.
The following example implements readInt : IO Int which reads a line from the input and parses it to an integer.
Running the code evaluates main, which does nothing, and just returns the value of readInt β which is a constant.
However, when executing main, it reads a line of input!
To execute an I/O effect, write
:exec some_io_valueto the REPL or usestlcpp --exec file.stlc. Neither the course platform or the playground supports executing code as of now.
The readInt function first uses io.readline : IO String to get an βimpureβ string value.
Then it parses the string into an Int using int.parse : String -> Unit + Int and defaults to 0 if parsing fails.
Finally it returns the parsed value wrapped up as IO Int using the io.pure function.
Notice that the line between what is considered a function and what is a constant has faded away.
readIntis not an arrow type, but itβs still usually considered to be a function which just doesnβt take any input.
Writing interactive programs
The standard library provides a convenient function io.repl : (String -> String) -> IO Unit that can be used to easily write interactive programs.
The REPL exits on the first empty line.
Ξ» :exec io.repl (int.to_string . count Char)
hello
5
aΓΆlksdfjaslΓΆfdj
15
However, to write more complex IO-operations, one has to utilize the full power of io.bind.
Chaining IO Using io.bind
To do something with the result of readInt, we can bind it to any function of type Int -> IO B.
For instance, we could convert the integer to a string and then print it:
printGuess : Int -> IO Unit
printGuess = fun n : Int, io.print ("Your number: " <> int.to_string n)
And finally, to compose the readInt and printGuess functions, we use io.bind which has the following signature
io.bind : forall A, forall B, (A -> IO B) -> IO A -> IO B
Together, these form the following program.
When executed, the program reads an integer as input and prints a message containing the integer.
- Input:
123 - Output:
Your number: 123
Next, your task is to implement a similar program in the following assignment.
Footnotes
Footnotes
-
Monads, like lists, are one of the most ubiquitous type constructors in functional programming languages. This course wonβt go into detail on why monads are so useful, but if you want, you can learn more about monads in HaskellWiki. β©