Unit 4. IO in Haskell

So far in this book, you’ve seen many examples of the powerful things you can do with Haskell. A recurring theme of Haskell is that much of this power comes from simple things such as referential transparency and Haskell’s type system. But there has been one glaring omission so far: I/O.

No matter what your program does, no matter what language it’s written in, I/O is a hugely important part of software. It’s the point where your code meets the real world. So why haven’t you seen much Haskell involving I/O yet? The problem is that using I/O inherently requires you to change the world. Take, for example, getting user input from the command line. Each time you have a program that requests user input, you expect the result to be different. But in unit 1 we spent a great deal of time talking about how important it is that all functions take an argument, return a value, and always return the same value for the same argument. Another issue with I/O is that you’re always changing the world, which means you’re dealing with state. If you read a file and write to another, your programs would be useless if you didn’t change the world somewhere along the way. But again, avoiding state is one of the key virtues of Haskell discussed in unit 1.

So how does Haskell solve this problem? As you might expect, Haskell does this by using types. Haskell has a special parameterized type called IO. Any value in an IO context must stay in this context. This prevents code that’s pure (meaning it upholds referential transparency and doesn’t change state) and code that’s necessarily impure from mixing.

To demonstrate this, you’ll compare two similar mystery functions in both Java and Haskell. You’ll start by taking a look at two nearly identical Java methods, called mystery1 and mystery2.

Listing 1. Two Java methods with the same type signature, mystery1 and mystery2
public class Example {

    public static int mystery1(int val1, int val2){
        int val3 = 3;
        return Math.pow(val1 + val2 + val3, 2);
    }

    public static int mystery2(int val1, int val2){
        int val3 = 3;
        System.out.print("Enter a number");
        try {
            Scanner in = new Scanner(System.in);
            val3 = in.nextInt();
        } catch (IOException e) {

            e.printStackTrace();
        }
        return Math.pow(val1 + val2 + val3,2);
    }
}

Here you have two static methods, mystery1 and mystery2. Both do the same thing: they take in two values, add them with a mystery value, and square the result. What’s most important is that these methods have identical type signatures in Java. But I don’t think anyone would argue that these methods are remotely the same!

The mystery1 method is predictable. Every time you enter two inputs, you’ll get the exact same output. In Haskell terms, mystery1 is a pure function. If you play around with this function enough, you’ll eventually be able to figure out what it does.

The mystery2 method, on the other hand, is a different method. Every time you call mystery2, many things can go wrong. Additionally, every time you call mystery2, you’re likely to get a different answer. You may never be able to figure out what mystery2 is doing. Now, in this example you could clearly tell the difference because mystery2 will force a command prompt. But suppose mystery2 just read from a random file on disk. You might never know what it was doing. The idea of mystery functions may seem contrived, but anytime you use legacy code or an external library, you’re often dealing with mystery functions: you may easily understand them from their behavior, but have no way of knowing what they’re doing.

Haskell solves this problem by forcing these two functions to be different types. Whenever a function uses IO, the results of that function are forever marked as coming from IO. Here are the two Java methods rewritten as Haskell functions.

Listing 2. mystery1 and mystery2 rewritten in Haskell
mystery1 :: Int -> Int -> Int
mystery1 val1 val2 = (val1 + val2 + val3)^2
 where val3 = 3

mystery2 :: Int -> Int -> IO Int
mystery2 val1 val2 = do
   putStrLn "Enter a number"
   val3Input <- getLine
   let val3 = read val3Input
   return ((val1 + val2 + val3)^2)

Why does this IO type make your code safer? IO makes it impossible to accidentally use values that have been tainted with I/O in other, pure functions. For example, addition is a pure function, so you can add the results of two calls to mystery1:

safeValue = (mystery1 2 4) + (mystery1 5 6)

But if you try to do the same thing, you’ll get a compiler error:

unsafeValue = (mystery2 2 4) + (mystery2 2 4)
"No instance for (Num (IO Int)) arising from a use of '+'"

Although this certainly adds safety to your program, how in the world are you going to do things? In this unit, you’ll focus on learning the Haskell tools that enable you to keep your pure code separated from I/O code and still make useful programs that interact with the real world. After this unit, you’ll be able to use Haskell for a wide range of everyday, real-world programming problems that involve using I/O.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.69.201