Introduction

Haskell famously has ‘monadic IO’, which allows us to compose IO actions with bind. For example, here’s a crude version of cp:1

cp ifn ofn = readFile ifn >>= writeFile ofn

In practice we would normally use do-notation:

cp ifn ofn = do
                txt <- readFile ifn
		writeFile ofn txt

Unless of course we’re working in ghci, where one-line expressions seem much more convenient to me—perhaps because I wrote a lot of Perl in the past.

Messing around in ghci

In the example above, bind cleanly chains the IO actions. As a reminder it has type:

(>>=) :: Monad m => m a -> (a -> m b) -> m b

Sometimes though we don’t want to put the data into another IO action, but rather analyse it, hopefully with a pure function. Bind looks less helpful here because our results will typically not be in IO i.e. we have (a -> b) not (a -> m b).

Staying with bind for now though, we’ll need a way to make an IO action to display our results. Enter print:

print :: Show a => a -> IO ()

So we could make a wc2 clone thus:

ghci> readFile "/usr/share/dict/words" >>= print . length . words
235886

Gosh that’s ugly! Remembering that the ghci prompt is a bit like a do-block, perhaps a multi-line approach would help:

ghci> dict <- readFile "/usr/share/dict/words"
ghci> print . length . words $ dict
235886

Or, because dict is just a String to the ghci prompt:

ghci> length . words $ dict
235886

It is perhaps worth pointing out that ghci gives us considerable latitude to provide a result for it to print: these all look the same but have different types:

print  . length . words $ dict :: IO ()
         length . words $ dict :: Int
return . length . words $ dict :: Monad m => m Int

Anyway, all of these multi-line approaches are a bit fiddly in ghci because you have to re-execute multiple lines if you reload files.

It’s worth noting that using bind forces us to make an action, and so requires the print. However, we can improve things by using liftM from Control.Monad:

ghci> liftM (length . words) $ readFile "/usr/share/dict/words"
235886

ghci is happy to print this IO Int. I think this is better than the bind approaches, but it’s still quite noisy.

Applicative IO

Now, where there’s a monad, there’s also an applicative,3 and it struck me the other day this this applies to IO. So, we can actually write wc as:

ghci> length . words <$> readFile "/usr/share/dict/words"
235886

which strikes me as nice and simple.

We can’t use applicative for all the IO tasks in ghci: we will need the full power of the monad to collapse the IO (IO a)) we get from chaining IO actions directly. For example, the result above is in IO, so even if we convert it into a String we can't chain it to writeFile:

ghci> :t length . words <$> readFile "/usr/share/dict/words"
length . words <$> readFile "/usr/share/dict/words" :: IO Int

ghci> writeFile "foo" $  show . length . words
                     <$> readFile "/usr/share/dict/words"

<interactive>:19:20: error:
    • Couldn't match type ‘IO’ with ‘[]’
      Expected type: String
        Actual type: IO String
...	

However, for a class of tasks, I think using applicative is a nice simplification. We are not just limited to a single action. Here’s a very crude riff on diff:4

ghci> (==) <$> readFile "/usr/share/dict/words"
           <*> readFile "/usr/share/dict/words"
True

ghci> (==) <$> readFile "/usr/share/dict/words"
           <*> readFile "/usr/share/dict/propernames"
False

It is even easy to diff against a fixed string:

ghci> (==) <$> readFile "/usr/share/dict/words" <*> pure "Banana"
False

Functor IO

For the simple case of one argument, we could also replace the applicative <$> with fmap:

ghci> fmap (length . words) $ readFile "/usr/share/dict/words"
235886

but I think that’s less clear.

Conclusions

I think it’s often pretty useful and productive to explore code from the ghci prompt. However, things got messy if part of the exploration needed data from files, which discouraged me from doing the right thing.

I think using IO’s applicative instance solves a lot of the problems, and am somewhat annoyed I didn’t think of it before.