I/O, Yay!!

Lecture #6
Complete the associated in-class exercises.

Table of Contents
module LectureIO where

Um, yeah. OK. Maybe I/O is not that scary in most languages. To be honest, though, it’s pretty messy in most languages. How lovely really is java.util.Scanner, std::cin, scanf, etc.?

1 What are Monads?

A monad in Haskell is an instance m of the Monad type class. The type class says that m must be a type constructor of one argument (like ProVal or list) with two operations:

return :: a -> m a
This “injects” a value into the monad in some trivial way. If we think of
the monad as a “container” (like a Proval Double is a container for a
Double along with some provenance commentary or a [Double] is a
container for many doubles arranged in an order), then this constructs a
container capable of holding as, with nothing inside but its argument.
So, for ProVal, return x might make a new ProVal with x inside
and no commentary. For lists, return x makes the list [x].
(>>=) :: m a -> (a -> m b) -> m b
Also known as “bind”, this lets us build a new container (an m b)
by taking an original container (m a) and a function that tells us
what m b to “graft in” as a replacement for each a in the
container. It makes a new container representing the result of that
grafting.1

1.1 You Can Check Out Anytime You Like, But You Can Never Leave2

If you take a close look at the signatures of return :: a -> m a and (>>=) :: m a -> (a -> m b) -> m b, you’ll notice:

  1. It’s easy to “get a value out” of m and do something with it. You can use ma >>= f. Now, f can access the (or every) a inside the ma “container” and act on it.

    We can even define a new function using return and >>=:

    fmap :: Monad m => (a -> b) -> (m a -> m b)
    fmap f ma = ma >>= (\a -> return (f a))

    fmap f lets us use any normal function f that we want as a function that operates on values in any monad instead! It “lifts” f into the monad like a generalized version of map.3

    (So, we can “check out” of the m monad anytime we want to and get at the regular values inside.)

  2. However, the result types of return and >>= make it so that in the end, anytime we operate on a value in an m container, the result ends up in an m container as well!

    (So, we can never really leave the monad, at least not with only the Monad type class’s operations.)

Haskell defines a huge number of operations and functions (and even special syntax4) we can now use to manipulate anything that instantiates Monad, and we can take advantage of all of them if we make a Monad instance.

Here’s one simple one. >> is just like >>= except that when we “graft in” a m b, it’s always the same m b. It ignores the a it’s replacing:

(>>) :: Monad m => m a -> m b -> m b
ma >> mb   = ma >>= (\_ -> mb)
-- (\_ -> mb) is a little lambda function that
-- takes an argument, ignores it, and returns mb.

1.2 Our First Monad Instance

Here’s an updated version of ProVal:

data ProVal a = ProVal a [String]
  deriving (Eq, Ord, Show, Read)

Rather than a single String for comments, it has a list of Strings. That also means that a “plain” value x can simply have no comments: ProVal x [].

Let’s make it an instance of Monad! return makes the simplest ProVal we can. pa >>= f takes the a value from pa, feeds it to f, gets the ProVal b it produces, and finally prepends all of pa’s comments onto the ProVal b’s comments. (So, we end up with the b value, but we carry all the provenance comments along, in the correct order.)

instance Monad ProVal where
  ...

(Two exercises.)

2 Why Would You Bother, or, What’s So Scary About I/O?

The trouble with I/O (and side effects in general) in Haskell are (1) they aren’t functional and (2) lazy evaluation means we don’t even know when (or if) they happen.

But, what if we made a new monad called IO? An IO a is a container that describes:

For the IO monad, return x makes a “fake” action that just produces x when run.

ioa >>= f makes a new action that, when run:

Did you see the trick there? >>= solves our two problems above: (1) It produces an action value without actually performing the action. No side effects! (2) But that value represents an action that does what we want in the correct order.

Now, we can build up actions like “read in the user’s name >>= given string s, say hello to s”, which produces an action that, when run, first reads in the user’s name and then feeds that name into code that says hello to them.

-- | An action to read in the user's name.
-- Remember that ioa >> iob produces an action that
-- performs ioa and then performs iob, producing iob's
-- result. (So, like >>=, but ignoring ioa's value.)
readName :: IO String
readName = putStrLn "What is your name?" >> getLine

-- | An action to say hello to s.
-- () is a special type with only one value in it: ()
-- It is similar in a sense to the void type in Java.
-- (The type is called "unit" when you read it aloud.)
sayHello :: String -> IO ()
sayHello s = putStrLn ("Hello, " ++ s ++ "!")

Now, let’s make this file a program that reads a user’s name and greets them, using readName and sayHello above and >>=:

-- | When you run a program in Haskell, it is `main` 
-- that is run. Its `IO ()` action is created and then
-- ACTUALLY RUN, which is magical and beyond the scope
-- of normal Haskell.
--
-- In ghci, any `IO a` we evaluate at the prompt is also run.
main :: IO ()
main = greet

-- | Get a user's name and greet them.
-- We can use our helpers above to write this.
greet :: IO ()
greet = undefined

We’ll just run it in ghci.5

(Four exercises.)

3 Our Game(s) With IO

We should really make a version of our game(s) with a user interface. For lack of time, we’re just going to give it to you for you to look through rather than go through it step-by-step. Try it out and investigate the code!

Here’s a playable game (minus one piece we suggest as an exercise for you!).

4 Readings

5 Appendix

Here are some extra notes on I/O in Haskell.

5.1 The Trouble with I/O

Simon Peyton Jones ruefully and authoritatively describes the trouble with I/O in Tackling the Awkward Squad: “A purely functional program implements a function; it has no side effect. Yet the ultimate purpose of running a program is invariably to cause some side effect: a changed file, some new pixels on the screen, a message sent, or whatever. Indeed it’s a bit cheeky to call input/output”awkward” at all. I/O is the raison d’être of every program — a program that had no observable effect whatsoever (no input, no output) would not be very useful.”

5.2 The Bad Old Days

We discussed (and Peyton Jones points out) lazy evaluation makes the I/O problem even worse. We don’t know when the side effects (like I/O) will actually happen, or even if they will happen!

Laziness also presents one potential solution: Just model the input to the program as a potentially-infinite list of “input objects” from the world and output by the program as a potentially-infinite list of “output objects”. An output could be requesting some information, which will hopefully eventually come back in on the input.

Haskell used to use this solution.. but it has lots of problems resolved by monads!


  1. In fact, there are more requirements for a correct monad instance, and Monad has superclasses. So, we have a bit more work to do, but often this work is straightforward if we sensibly handle return and >>=.↩︎

  2. I’m not sure what source I read that compared Monads to the Eagles’ Hotel California line: “You can check out any time you like, but you can never leave!” But, it’s too fun not to use!↩︎

  3. In fact, fmap is defined by the Functor typeclass, which generalizes map. A Functor is a “container” like a Monad, but the key operation it supports is “lifting” a function up so that it operates “in the natural way” within the container.↩︎

  4. do notation is a little language built into Haskell that you can use to make monadic programming look remarkably like imperative programming in Java. My advice is do not use it until you feel like you understand return, >>=, and >>.↩︎

  5. To run this with ghc: comment out the module line at the top of the file, run ghc Lecture6.lhs, and then run the executable that produces (./Lecture6, in Unix).↩︎