Introduction to Functional Programming

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.pure constructor and io.bind operator.
  • You can write simple programs involving multiple I/O operations.

Important!

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

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:

  1. being able to also perform pure calculations with impure inputs, and
  2. 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.

No files opened Select a file to edit
No files opened Select a file to edit

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.

Infinite Loops Are Impure

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.

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.

No files opened Select a file to edit

However, when executing main, it reads a line of input!

To execute an I/O effect, write :exec some_io_value to the π•Šπ•‹π•ƒβ„‚++ REPL or use stlcpp --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. readInt is 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.

No files opened Select a file to edit

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.

Loading Exercise...
Loading Exercise...

Footnotes

Footnotes

  1. 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. ↩