1.5 Performing effects

Up until this point, we have written functions that operate in a pure context, which does not allow performing any kind of input or output. Instead, we ran them inside of ghci, which conveniently displays the result.

Haskell makes a clear distinction between parts of your code that are allowed to have effects (e.g. printing to the console, reading a file, accessing a database) and parts that are pure (no effects allowed). Code that is allowed to have effects is labeled with IO in its type signature. We say that this code operates in an IO context. Anything with a type of the form IO something can be called an IO action. You can run an IO action to perform an effect and optionally produce some result. The type of this result will be the same as the something in the type signature.

To read input from the console, we can use an IO action called getLine.

getLine :: IO String

When you run this IO action, it waits for you to type something into the console and hit the Enter or Return key, after which it produces a value of type String that is exactly what you typed into the console.

To print to the console, we can use a function called putStrLn.

putStrLn :: String -> IO ()

It is a function that takes a String and produces an IO action with a result of (), which we call Unit. Unit represents no value, meaning that this IO action does not have a result, but it still performs some kind of effect. In this case, the effect is printing to the console.

The entry point to every program is called main, and you can think of it as one huge IO action that contains your entire program. The type of main is always IO (). Here is a simple example.

main :: IO ()
main = putStrLn "Beam me up, Scotty!"

This program prints the message “Beam me up, Scotty!” to the console and then terminates. Type this program into a file called Beam.hs, save it, and run it using runghc, like so:

$ runghc Beam.hs
Beam me up, Scotty!

You can see that, since we applied putStrLn to a String, the whole expression now has the type IO (), which matches the type for main. This is important, because you can’t just use whatever expression you want—the types need to match up. For example, the following definition for main will result in an error.

fuelCores :: Integer
fuelCores = 3

main :: IO ()
main = fuelCores

The type of this expression is Integer, but main must have the type IO (), so they do not match. If we save this to a file BadIO.hs and run it, we get the following error:

$ runghc BadIO.hs

BadIO.hs:5:8: error:
    • Couldn't match expected type ‘IO ()’ with actual type ‘Integer’
    • In the expression: fuelCores
      In an equation for ‘main’: main = fuelCores
  |
5 | main = fuelCores
  |        ^^^^^^^^^

How about a more complex program that does both input and output?

main :: IO ()
main = getLine >>= putStrLn

This program uses getLine to read a line of text from the console, and then uses putStrLn to print it back out again. It echoes back whatever you type into the console. Create a new file called Echo.hs, add this definition to it, then run it in your console. Once it is running, type any message you want, then hit the Enter or Return key.

$ runghc Echo.hs
Hello from outer space
Hello from outer space

It should display the same message that you just typed. How does it actually work, though? You may have noticed a new operator >>= used in this expression.

(>>=) :: IO a -> (a -> IO b) -> IO b

This is an infix function called bind. If we look at the type signature, this operator takes two inputs, the first is an IO action, and the second is a function that produces an IO action. It takes the result of the IO action on the left-hand side, and then applies the right-hand function to this value to produce a new IO action. You can look at it as a way to combine together multiple IO actions, where the result of each consecutive IO action may depend on the previous one. In our program above, getLine is the first IO action, and putStrLn is the function that takes the result of getLine and transforms it into a new IO action. By connecting these together using >>=, we get a new IO action that reads from the console and prints it back out again.

Something peculiar about the type for >>= is that it contains type variables, specifically a and b. Just like you can have variables in functions, you can have variables in types. You may have noticed by now that all types in Haskell begin with an uppercase letter. By contrast, type variables begin with a lowercase letter. If we want to figure out the actual types that these variables represent when this function is used in a specific expression, we must unify the types through a process called type unification.

Type unification

Let’s unify the types for the following expression.

getLine >>= putStrLn

We already know the types for each subexpression.

(>>=) :: IO a -> (a -> IO b) -> IO b
getLine :: IO String
putStrLn :: String -> IO ()

If we want to unify the type signature for >>= with this expression to figure out what the type variables actually are, we look at each input, one by one. Starting with the first input, we must unify the type of getLine with IO a.

firstInput :: IO a
getLine    :: IO String

The only way for these two types to be the same is if a is String. For the second input, we do this again, lining up the type signatures for clarity.

secondInput :: a      -> IO b
putStrLn    :: String -> IO ()

We already know from the first input that type variable a must be String, so let’s substitute for a.

secondInput :: String -> IO b
putStrLn    :: String -> IO ()

Since there is no conflict yet between these two type signatures, we can continue. Type variable b must be () for the types to unify. Now that we know the actual types for the type variables, we can substitute them into the original type signature for >>=.

(>>=) :: IO String -> (String -> IO ()) -> IO ()

Type unification is a helpful tool for reasoning about how parts of your program fit together. The compiler does this automatically for you, and will tell you when the types are mismatched, as we saw in an earlier example. Eventually, you will have an intuition for how types will unify without having to work it out on paper. But that’s enough about type unification for now. It’s time to continue exploring effects in Haskell.

More on effects

Besides the bind >>= operator, there is another important function that you may find useful when working in an IO context.

pure :: a -> IO a

The function pure takes a normal, pure value, and puts it into an IO context. Running the IO action that this function produces will not actually perform any effects, but it will produce, as a result, whatever value you used to construct the IO action. For example, we can take the program we wrote earlier in Beam.hs,

main :: IO ()
main = putStrLn "Beam me up, Scotty!"

and write it in a different way using pure,

main :: IO ()
main = pure "Beam me up, Scotty" >>= putStrLn

but still get the same end result.

$ runghc Beam.hs
Beam me up, Scotty!

You will find a use for pure in the project for this chapter. So far, we have written a program that displays a message in the console, and one that echoes back whatever you type. Let’s do something more interesting with the input.

main :: IO ()
main = getLine >>= (\ name -> putStrLn (beam name))

beam :: String -> String
beam name = "Beam me up, " ++ name ++ "!"

Here, we are using a lambda expression to bind the input from the console to the variable name, to which we then apply a function beam that uses it to construct a message. Finally, this message is printed to the console using putStrLn. In the function beam, you should see a new operator ++ called append.

(++) :: String -> String -> String

You can use this operator to join two strings together. Update your Beam.hs file with the program above and run it. Once it is running, type your name into the console and hit the Enter or Return key.

$ runghc Beam.hs
Spock
Beam me up, Spock!

We can simplify our program by removing the parentheses surrounding the lambda expression. It will still work as expected, because the precedence rules for >>= are set up in such a way that it automatically groups everything on the right-hand side together.

main :: IO ()
main = getLine >>= \ name -> putStrLn (beam name)

In fact, we can even move everything after the lambda arrow -> down to the next line, if we wish, and it will still work just fine. This is helpful for avoiding long lines, making large expressions easier to read.

main :: IO ()
main = getLine >>= \ name ->
  putStrLn (beam name)

One way we could improve this program is to add a prompt asking for your name, so that when you run the program, you will be greeted by a clear message instead of having it look like nothing happened. For this, we can use another function that is very similar to putStrLn called putStr. The only difference is that putStr does not add a newline character at the end of the String, meaning that it does not force your cursor onto the next line.

main :: IO ()
main = prompt >>= \ x ->
  getLine >>= \ name ->
  putStrLn (beam name)

beam :: String -> String
beam name = "Beam me up, " ++ name ++ "!"

prompt :: IO ()
prompt = putStr "What is your name? "

Save this in Beam.hs and run it.

$ runghc Beam.hs
What is your name? Spock
Beam me up, Spock!

That is much better, but in the code, we have this extra x variable that isn’t being used for anything at all, because the result from putStr is just (). There is a special pattern we can use in cases like this, where we need to match on something, but don’t care about the value. That pattern is _, and it can be used as many times as you like in the same expression.

> (\ _ -> "Ignoring the input") 8
"Ignoring the input"

> (\ _ _ _ -> "Ignoring all three inputs") 1 2 3
"Ignoring all three inputs"

We can make that simplification to our program like so.

main :: IO ()
main = prompt >>= \ _ ->
  getLine >>= \ name ->
  putStrLn (beam name)

This situation is so common for >>= that there is a special version of bind called sequence >>.

(>>) :: IO a -> IO b
a >> b = a >>= \ _ -> b

You can use >> to combine together two IO actions, ignoring the result from the first one. If we make that additional change to our program, we now have the following definition for main.

main :: IO ()
main = prompt >>
  getLine >>= \ name ->
  putStrLn (beam name)

You are now ready to write your first, real Haskell program as part of the project for this chapter.