Haskell – 157 – IO, input/output – 1

Continuo da qui, copio qui.

As we mentioned earlier, it is difficult to think of a good, clean way to integrate operations like input/output into a pure functional language. Before we give the solution, let’s take a step back and think about the difficulties inherent in such a task.

Any IO library should provide a host of functions, containing (at a minimum) operations like:

  • print a string to the screen
  • read a string from a keyboard
  • write data to a file
  • read data from a file

There are two issues here. Let’s first consider the initial two examples and think about what their types should be. Certainly the first operation (I hesitate to call it a “function”) should take a String argument and produce something, but what should it produce? It could produce a unit (), since there is essentially no return value from printing a string. The second operation, similarly, should return a String, but it doesn’t seem to require an argument.

We want both of these operations to be functions, but they are by definition not functions. The item that reads a string from the keyboard cannot be a function, as it will not return the same String every time. And if the first function simply returns () every time, there should be no problem with replacing it with a function f _ = (), due to referential transparency. But clearly this does not have the desired effect.

La soluzione RealWorld
In a sense, the reason that these items are not functions is that they interact with the “real world.” Their values depend directly on the real world. Supposing we had a type RealWorld, we might write these functions as having type:

printAString :: RealWorld -> String -> RealWorld
readAString  :: RealWorld -> (RealWorld, String)

That is, printAString takes a current state of the world and a string to print; it then modifies the state of the world in such a way that the string is now printed and returns this new value. Similarly, readAString takes a current state of the world and returns a new state of the world, paired with the String that was typed.

This would be a possible way to do IO, though it is more than somewhat unwieldy. In this style (assuming an initial RealWorld state were an argument to main), our “Name.hs” program from the section on Interactivity would look something like:

main rW =
  let rW' = printAString rW "Please enter your name: "
      (rW'',name) = readAString rW'
  in  printAString rW''
          ("Hello, " ++ name ++ ", how are you?")

This is not only hard to read, but prone to error, if you accidentally use the wrong version of the RealWorld. It also doesn’t model the fact that the program below makes no sense:

main rW =
  let rW' = printAString rW "Please enter your name: "
      (rW'',name) = readAString rW'
  in  printAString rW'                 -- OOPS!
          ("Hello, " ++ name ++ ", how are you?")

In this program, the reference to rW'' on the last line has been changed to a reference to rW'. It is completely unclear what this program should do. Clearly, it must read a string in order to have a value for name to be printed. But that means that the RealWorld has been updated. However, then we try to ignore this update by using an “old version” of the RealWorld. There is clearly something wrong happening here.

Suffice it to say that doing IO operations in a pure lazy functional language is not trivial.

Azioni
The breakthrough for solving this problem came when Phil Wadler realized that monads would be a good way to think about IO computations. In fact, monads are able to express much more than just the simple operations described above; we can use them to express a variety of constructions like concurrence, exceptions, IO, non-determinism and much more. Moreover, there is nothing special about them; they can be defined within Haskell with no special handling from the compiler (though compilers often choose to optimize monadic operations).

As pointed out before, we cannot think of things like “print a string to the screen” or “read data from a file” as functions, since they are not (in the pure mathematical sense). Therefore, we give them another name: actions. Not only do we give them a special name, we give them a special type. One particularly useful action is putStrLn, which prints a string to the screen. This action has type:

putStrLn :: String -> IO ()

As expected, putStrLn takes a string argument. What it returns is of type IO (). This means that this function is actually an action (that is what the IO means). Furthermore, when this action is evaluated (or “run”) , the result will have type ().

Note: Actually, this type means that putStrLn is an action within the IO monad, but we will gloss over this for now.

You can probably already guess the type of getLine:

getLine :: IO String

This means that getLine is an IO action that, when run, will have type String.

The question immediately arises: “how do you ‘run’ an action?”. This is something that is left up to the compiler. You cannot actually run an action yourself; instead, a program is, itself, a single action that is run when the compiled program is executed. Thus, the compiler requires that the main function have type IO (), which means that it is an IO action that returns nothing. The compiled code then executes this action.

However, while you are not allowed to run actions yourself, you are allowed to combine actions. In fact, we have already seen one way to do this using the do notation (how to really do this will be revealed in the chapter Monads). Let’s consider the original name program:

main = do
  hSetBuffering stdin LineBuffering
  putStrLn "Please enter your name: "
  name <- getLine
  putStrLn ("Hello, " ++ name ++ ", how are you?")

We can consider the do notation as a way to combine a sequence of actions. Moreover, the <- notation is a way to get the value out of an action. So, in this program, we’re sequencing four actions: setting buffering, a putStrLn, a getLine and another putStrLn. The putStrLn action has type String -> IO (), so we provide it a String, so the fully applied action has type IO (). This is something that we are allowed to execute.

The getLine action has type IO String, so it is okay to execute it directly. However, in order to get the value out of the action, we write name <- getLine, which basically means “run getLine, and put the results in the variable called name.”

Normal Haskell constructions like if/then/else and case/of can be used within the do notation, but you need to be somewhat careful. For instance, in our “guess the number” program, we have:

  do ...
     if (read guess)  num
              then do putStrLn "Too high!"
                      doGuessing num
              else do putStrLn "You Win!"

If we think about how the if/then/else construction works, it essentially takes three arguments: the condition, the “then” branch, and the “else” branch. The condition needs to have type Bool, and the two branches can have any type, provided that they have the same type. The type of the entire if/then/else construction is then the type of the two branches.

In the outermost comparison, we have (read guess) < num as the condition. This clearly has the correct type. Let’s just consider the “then” branch. The code here is:

                    do putStrLn "Too low!"
                       doGuessing num

Here, we are sequencing two actions: putStrLn and doGuessing. The first has type IO (), which is fine. The second also has type IO (), which is fine. The type result of the entire computation is precisely the type of the final computation. Thus, the type of the “then” branch is also IO (). A similar argument shows that the type of the “else” branch is also IO (). This means the type of the entire if/then/else construction is IO (), which is just what we want.

Note: In this code, the last line is else do putStrLn "You Win!". This is somewhat overly verbose. In fact, else putStrLn "You Win!" would have been sufficient, since do is only necessary to sequence actions. Since we have only one action here, it is superfluous.

It is incorrect to think to yourself “Well, I already started a do block; I don’t need another one,” and hence write something like:

    do if (read guess) < num
         then putStrLn "Too low!"
              doGuessing num
         else ...

Here, since we didn’t repeat the do, the compiler doesn’t know that the putStrLn and doGuessing calls are supposed to be sequenced, and the compiler will think you’re trying to call putStrLn with three arguments: the string, the function doGuessing and the integer num. It will certainly complain (though the error may be somewhat difficult to comprehend at this point).

We can write the same doGuessing function using a case statement. To do this, we first introduce the Prelude function compare, which takes two values of the same type (in the Ord class) and returns one of GT, LT, EQ, depending on whether the first is greater than, less than or equal to the second.

doGuessing num = do
  putStrLn "Enter your guess:"
  guess  do putStrLn "Too low!"
             doGuessing num
    GT -> do putStrLn "Too high!"
             doGuessing num
    EQ -> putStrLn "You Win!"

Here, again, the dos after the ->s are necessary on the first two options, because we are sequencing actions.

If you’re used to programming in an imperative language like C or Java, you might think that return will exit you from the current function. This is not so in Haskell. In Haskell, return simply takes a normal value (for instance, one of type Int) and makes it into an action that returns the given value (for instance, the value of type IO Int). In particular, in an imperative language, you might write this function as:

void doGuessing(int num) {
  print "Enter your guess:";
  int guess = atoi(readLine());
  if (guess == num) {
    print "You win!";
    return ();
  }

  // we won't get here if guess == num
  if (guess < num) {
    print "Too low!";
    doGuessing(num);
  } else {
    print "Too high!";
    doGuessing(num);
  }
}

Here, because we have the return () in the first if match, we expect the code to exit there (and in most imperative languages, it does). However, the equivalent code in Haskell, which might look something like:

doGuessing num = do
  putStrLn "Enter your guess:"
  guess  do putStrLn "You win!"
             return ()

  -- we don't expect to get here unless guess == num
  if (read guess < num)
    then do putStrLn "Too low!";
            doGuessing num
    else do putStrLn "Too high!";
            doGuessing num

will not behave as you expect. First of all, if you guess correctly, it will first print “You win!,” but it won’t exit, and it will check whether guess is less than num. Of course it is not, so the else branch is taken, and it will print “Too high!” and then ask you to guess again.

On the other hand, if you guess incorrectly, it will try to evaluate the case statement and get either LT or GT as the result of the compare. In either case, it won’t have a pattern that matches, and the program will fail immediately with an exception.

esercizi
Write a program that asks the user for his or her name. If the name is one of Simon, John or Phil, tell the user that you think Haskell is a great programming language. If the name is Koen, tell them that you think debugging Haskell is fun (Koen Classen is one of the people who works on Haskell debugging); otherwise, tell the user that you don’t know who he or she is.

Write two different versions of this program, one using if statements, the other using a case statement.

Using if, we get something like:

main = do
  hSetBuffering stdin LineBuffering
  putStrLn "Please enter your name:"
  name <- getLine
  if name == "Simon" || name == "John" || name == "Phil"
    then putStrLn "Haskell is great!"
    else if name == "Koen"
           then putStrLn "Debugging Haskell is fun!"
           else putStrLn "I don't know who you are."

Note that we don’t need to repeat the dos inside the ifs, since these are only one action commands.

We could also be a bit smarter and use the elem command which is built in to the Prelude:

main = do
  hSetBuffering stdin LineBuffering
  putStrLn "Please enter your name:"
  name <- getLine
  if name `elem` ["Simon", "John", "Phil"]
    then putStrLn "Haskell is great!"
    else if name == "Koen"
           then putStrLn "Debugging Haskell is fun!"
           else putStrLn "I don't know who you are."

Of course, we needn’t put all the putStrLns inside the if statements. We could instead write:

main = do
  hSetBuffering stdin LineBuffering
  putStrLn "Please enter your name:"
  name <- getLine
  putStrLn
    (if name `elem` ["Simon", "John", "Phil"]
       then "Haskell is great!"
       else if name == "Koen"
              then "Debugging Haskell is fun!"
              else "I don't know who you are.")

Using case, we get something like:

main = do
  hSetBuffering stdin LineBuffering
  putStrLn "Please enter your name:"
  name  putStrLn "Haskell is great!"
    "John"  -> putStrLn "Haskell is great!"
    "Phil"  -> putStrLn "Haskell is great!"
    "Koen"  -> putStrLn "Debugging Haskell is fun!"
    _       -> putStrLn "I don't know who you are."

Which, in this case, is actually not much cleaner.

Nota: Hal dimentica di dirmi che devo importare la libreria di IO:

import System.IO

đź‘˝

Posta un commento o usa questo indirizzo per il trackback.

Trackback

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo di WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google photo

Stai commentando usando il tuo account Google. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.

%d blogger hanno fatto clic su Mi Piace per questo: