I/O, Yay!!
Complete the associated in-class exercises.
module LectureIO whereUm, 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.?
Haskell uses Monads to represent I/O.
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 Doubleis a container for a Doublealong 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 xmight make a newProValwithxinside - and no commentary. For lists,
return xmakes 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 bto “graft in” as a replacement for eachain 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:
It’s easy to “get a value out” of
mand do something with it. You can usema >>= f. Now,fcan access the (or every)ainside thema“container” and act on it.We can even define a new function using
returnand>>=:fmap :: Monad m => (a -> b) -> (m a -> m b) fmap f ma = ma >>= (\a -> return (f a))fmap flets us use any normal functionfthat we want as a function that operates on values in any monad instead! It “lifts”finto the monad like a generalized version ofmap.3(So, we can “check out” of the
mmonad anytime we want to and get at the regular values inside.)However, the result types of
returnand>>=make it so that in the end, anytime we operate on a value in anmcontainer, the result ends up in anmcontainer as well!(So, we can never really leave the monad, at least not with only the
Monadtype 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
...End of Day 1; we worked through most of the ProVal exercise. We also foreshadowed why we would bother already.
2 Today’s Notes 2024/10/16
- Quiz 3: Coming Friday! Go to the location indicated on PrairieTest! If you’re in DMP 110, have your laptop ready!
- Quiz retakes:
- Qualtrics survey coming soon, due Monday night, you will indicate whether you:
- Need 150 minutes or 110 minutes is sufficient?
- Have conflicts with 6-7PM, 7-9PM (announced time), or 9-9:30PM?
- Have concerns regarding using your own computer?
- On Tuesday, we’ll schedule everyone into sessions. We hope to provide 150 minutes to all. You
- You may take as many or as few retake quizzes during that time as you like. (You could even switch around.)
- DO NOT start a quiz unless you plan to retake it!
- Qualtrics survey coming soon, due Monday night, you will indicate whether you:
- Assignment 2:
- Early deadline is tomorrow!
- On-time deadline is Oct 28
- Haskell project proposal:
- Let’s take a few minutes to “match-make”.
- If you’re on a team and your team isn’t welcoming new members, work on in-class exercises or the assignment!
- Let’s take a few minutes to “match-make”.
(Two exercises.)
3 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:
- an action,
- possibly with side effects
- that, when run,
- produces an
avalue”.
For the IO monad, return x makes a “fake” (or trivial) action that just produces x when run.
ioa >>= f makes a new action that, when run:
- runs
ioafirst, - gets the
ait produces, - feeds it to
f, - runs the action that
freturns second, and - produces as its final value whatever that second action produced.
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 = readName >>= sayHelloWe’ll just run it in ghci.5
(Four exercises.)
4 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!).
5 Readings
Work through CIS 194 Lecture 8 up to but not including “Record syntax” (but record syntax is still handy to know!)
Work through CIS 194 Lecture 4 up to but not including “Folds”
Read about operator sections
6 Appendix
Here are some extra notes on I/O in Haskell.
6.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.”
6.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!
In fact, there are more requirements for a correct monad instance, and
Monadhas superclasses. So, we have a bit more work to do, but often this work is straightforward if we sensibly handlereturnand>>=.↩︎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!↩︎
In fact,
fmapis defined by theFunctortypeclass, which generalizesmap. AFunctoris a “container” like aMonad, but the key operation it supports is “lifting” a function up so that it operates “in the natural way” within the container.↩︎donotation is a little language built into Haskell that you can use to make monadic programming look remarkably like imperative programming in Java. My advice isdo notuse it until you feel like you understandreturn,>>=, and>>.↩︎To run this with
ghc: comment out themoduleline at the top of the file, runghc Lecture6.lhs, and then run the executable that produces (./Lecture6, in Unix).↩︎
