Haskell/简单的输入输出

维基教科书,自由的教学读本

迄今为止这本教材已经讨论了返回数值的函数,它们很不错。但是我们怎样才能写出"Hello world"?为了给你第一印象,这里有一个Haskell版的"Hello world"程序:

Example
Example

例子: Hello! What is your name?

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


这个小程序至少说明了haskell没有忘记提供输入输出功能(IO)! 因为IO会产生副作用,函数式语言基本上都在这方面存在问题。对于相同的参数,函数总是应该返回相同的结果。但是像getLine这样的函数怎么可能每次都返回相同的结果呢?在给出答案前,让我们来仔细思考一下这个问题的难点。

任何IO库至少应该提供以下功能:

  • 在屏幕上打印字符串
  • 从键盘上读取字符串
  • 向文件写入数据
  • 从文件中读取数据

这里有两个问题。 我们先考虑前面的两个例子应该是什么类型. 第一个操作 (叫它“函数”似乎不合适) 应该取一个String类型的参数并返回一些东西,它应该返回什么呢?它应该返回一个()单元,因为打印字符串本身什么都不生成。第二个操作跟第一个类似,应该返回String类型的值,但它应该不需要参数。

我们希望这两个操作是函数,但是从定义上说它们不是函数。从键盘读取字符串的操作每次都返回不同的String类型值。如果putStrLn每次都返回(),依照引用透明性,我们可以把这个函数替换为f _ = ()。很明显这样做是不能得到相同结果的。

动作(Actions)[编辑]

当Philip Wadler发现monads将会是一个解决IO计算的好方法时,这个问题有了突破性的进展。实际上,monads能够解决比上述简单操作更复杂的问题。我们可以用monads来解决一大类问题,比如并发、异常、IO、非确定性等等。而且,monads也不难在编译器的层面上处理(虽然这样,编译器经常会优化monadic操作)。在某种程度上monads经常被人们误解为是难以理解的。这些我们会稍后解释,目前我们只需要知道IO使用的monads,我们可以在不了解背后理论细节的情况下使用它(其实monads理论也不是非常复杂)。目前我们可以暂时忘记monads的存在了。

就像前面说过的, 我们不能把类似“在屏幕上打印字符串”或是“从文件中读数据”的操作作为函数, 因为它们不是函数(从纯数学的角度)。因此,我们给它另一个名字:“动作”。我们不仅给它一个特殊的名字,我们还给了它一个特殊的类型。 一个非常有用的动作是putStrLn,它在屏幕上打印字符串。这个行为的类型是:

putStrLn :: String -> IO ()

跟我们想的一样,putStrLn需要一个字符串参数。那它的返回类型IO ()是什么呢?这说明这个函数是一个IO动作(这就是IO的意义)。此外,当这个动作被“执行”(或是“运行”)时,结果的类型是()

注解

实际上这个类型说明putStrLn是一个“IO monad"中的动作, 但是目前我们会先忽略它。

你现在可能已经猜到了getLine的类型:

getLine :: IO String

这个类型说明getLine是一个IO动作,执行这个动作后会返回类型为String的结果.

眼前的事情是我们怎样运行一个动作?这个看起来是编译器该干的活。你不能直接运行一个动作,而是要通过main函数执行这个动作。main函数本身也是一个动作,运行编译后的程序就会直接执行这个动作。因此,编译器要求main函数是类型IO (),也就是一个什么都不返回的IO动作。(译者:然而这并不代表程序在这个运行期间不修改外部的世界如屏幕输出,文件修改等,而是这个动作在执行完后没有任何结果给予后继程序使用)

However, while you are not allowed to run actions yourself, you are allowed to combine actions. There are two ways to go about this. The one we will focus on in this chapter is the do notation, which provides a convenient means of putting actions together, and allows us to get useful things done in Haskell without having to understand what really happens. Lurking behind the do notation is the more explicit approach using the (>>=) operator, but we will not be ready to cover this until the chapter Understanding monads

虽然,你不能直接运行一个动作,但是你可以把同类型的动作组合 combine 起来。 有两种方法可以用,其中一种方法是使用do我们将会在这章详细介绍。隐藏在do背后的 是一个更显式的方法,显式使用(>>=),我们会在Understanding monads这章详细解释。

注解

Do notation is just syntactic sugar for (>>=). If you have experience with higher order functions, it might be worth starting with the latter approach and coming back here to see how do notation gets used. 实际上do(>>=)的语法糖。如果你有使用高阶函数的经验,可以直接阅读学习第二种方法然后回来这章学习如何使用do

Let's consider the following name program:

Example
Example

例子: What is your name?

main = do
  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 three actions: 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 run as a program.

练习

Write a program which asks the user for the base and height of a triangle, calculates its area and prints it to the screen. The interaction should look something like:

The base?
3.3
The height?
5.4
The area of that triangle is 8.91
Hint: you can use the function read to convert user strings like "3.3" into numbers like 3.3 and function show to convert a number into string.

Left arrow clarifications[编辑]

左箭头说明

The <- is optional[编辑]

While we are allowed to get a value out of certain actions like getLine, we certainly are not obliged to do so. For example, we could very well have written something like this:

从特定的动作如 getLine 中取值是被容许的,但并非是必要的.我们可以这样写:

Example
Example

例子: executing getLine directly

main = do
  putStrLn "Please enter your name: "
  getLine
  putStrLn ("Hello, how are you?")


Clearly, that isn't very useful: the whole point of prompting the user for his or her name was so that we could do something with the result. That being said, it is conceivable that one might wish to read a line and completely ignore the result. Omitting the <- will allow for that; the action will happen, but the data won't be stored anywhere.

显然, 这样写没有太多用处: 提示用户输入名字是为了能利用输入结果来做一些事情. 而上面的代码却可以理解为, 进行读取一行的操作, 忽略读取结果. 忽略 <- 就是这效果; 动作发生, 但结果未在任何地方保存.

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."

The <- can be used with any action (except the last)[编辑]

On the flip side, there are also very few restrictions which actions can have values obtained from them. Consider the following example, where we put the results of each action into a variable (except the last... more on that later):

Example
Example

例子: putting all results into a variable

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


The variable x gets the value out of its action, but that isn't very interesting because the action returns the unit value (). So while we could technically get the value out of any action, it isn't always worth it. But wait, what about that last action? Why can't we get a value out of that? Let's see what happens when we try:

Example
Example

例子: getting the value out of the last action

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

Whoops!

YourName.hs:5:2:
    The last statement in a 'do' construct must be an expression


This is a much more interesting example, but it requires a somewhat deeper understanding of Haskell than we currently have. Suffice it to say, whenever you use <- to get the value of an action, Haskell is always expecting another action to follow it. So the very last action better not have any <-s.

這是一個很有趣的例子,相對於我們現在所認知的它需要一些對 Haskell 更深入的瞭解。只要用一句話就夠了,那就是無論你何時要使用 <- 去取得一個動作的值,Hasekell 總是期待後面還會有其它動作。所以,在最後一個動作最好不要有這個 <-

Controlling actions[编辑]

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 a simple "guess the number" program, we have:

    doGuessing num = do
       putStrLn "Enter your guess:"
       guess <- getLine
       if (read guess) < num
         then do putStrLn "Too low!"
                 doGuessing num
         else 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.

注解

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 <- getLine
  case compare (read guess) num of
    LT -> do putStrLn "Too low!"
             doGuessing num
    GT -> do putStrLn "Too high!"
             doGuessing num
    EQ -> do 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 the same example, the action would be 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 <- getLine
  case compare (read guess) num of
    EQ -> do putStrLn "You win!"
             return ()

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

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.

练习

What does the following program print out?

main =
 do x <- getX
    putStrLn x

getX =
 do return "hello"
    return "aren't"
    return "these"
    return "returns"
    return "rather"
    return "pointless?"
Why?
练习

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.

Actions under the microscope[编辑]

Actions may look easy up to now, but they are actually a common stumbling block for new Haskellers. If you have run into trouble working with actions, you might consider looking to see if one of your problems or questions matches the cases below. It might be worth skimming this section now, and coming back to it when you actually experience trouble.

Mind your action types[编辑]

One temptation might be to simplify our program for getting a name and printing it back out. Here is one unsuccessful attempt:

Example
Example

例子: Why doesn't this work?

main =
 do putStrLn "What is your name? "
    putStrLn ("Hello " ++ getLine)

Ouch!

YourName.hs:3:26:
    Couldn't match expected type `[Char]'
           against inferred type `IO String'


Let us boil the example above down to its simplest form. Would you expect this program to compile?

Example
Example

例子: This still does not work

main =
 do putStrLn getLine


For the most part, this is the same (attempted) program, except that we've stripped off the superflous "What is your name" prompt as well as the polite "Hello". One trick to understanding this is to reason about it in terms of types. Let us compare:

putStrLn :: String -> IO ()
getLine  :: IO String

We can use the same mental machinery we learned in Type basics to figure how everything went wrong. Simply put, putStrLn is expecting a String as input. We do not have a String, but something tantalisingly close, an IO String. This represents an action that will give us a String when it's run. To obtain the String that putStrLn wants, we need to run the action, and we do that with the ever-handy left arrow, <-.

Example
Example

例子: This time it works

main =
 do name <- getLine
    putStrLn name

Working our way back up to the fancy example:

main =
 do putStrLn "What is your name? "
    name <- getLine
    putStrLn ("Hello " ++ name)


Now the name is the String we are looking for and everything is rolling again.

Mind your expression types too[编辑]

Fine, so we've made a big deal out of the idea that you can't use actions in situations that don't call for them. The converse of this is that you can't use non-actions in situations that DO expect actions. Say we want to greet the user, but this time we're so excited to meet them, we just have to SHOUT their name out:

Example
Example

例子: Exciting but incorrect. Why?

import Data.Char (toUpper)

main =
 do name <- getLine
    loudName <- makeLoud name
    putStrLn ("Hello " ++ loudName ++ "!")
    putStrLn ("Oh boy! Am I excited to meet you, " ++ loudName)

-- Don't worry too much about this function; it just capitalises a String
makeLoud :: String -> String
makeLoud s = map toUpper s
This goes wrong...
    Couldn't match expected type `IO' against inferred type `[]'
      Expected type: IO t
      Inferred type: String
    In a 'do' expression: loudName <- makeLoud name


This is quite similar to the problem we ran into above: we've got a mismatch between something that is expecting an IO type, and something which is not. This time, the cause is our use of the left arrow <-; we're trying to left arrow a value of makeLoud name, which really isn't left arrow material. It's basically the same mismatch we saw in the previous section, except now we're trying to use regular old String (the loud name) as an IO String, which clearly are not the same thing. The latter is an action, something to be run, whereas the former is just an expression minding its own business. Note that we cannot simply use loudName = makeLoud name because a do sequences actions, and loudName = makeLoud name is not an action.

So how do we extricate ourselves from this mess? We have a number of options:

  • We could find a way to turn makeLoud into an action, to make it return IO String. But this is not desirable, because the whole point of functional programming is to cleanly separate our side-effecting stuff (actions) from the pure and simple stuff. For example, what if we wanted to use makeLoud from some other, non-IO, function? An IO makeLoud is certainly possible (how?), but missing the point entirely.
  • We could use return to promote the loud name into an action, writing something like loudName <- return (makeLoud name). This is slightly better, in that we are at least leaving the makeLoud itself function nice and IO-free, whilst using it in an IO-compatible fashion. But it's still moderately clunky, because by virtue of left arrow, we're implying that there's action to be had -- how exciting! -- only to let our reader down with a somewhat anticlimatic return
  • Or we could use a let binding...

It turns out that Haskell has a special extra-convenient syntax for let bindings in actions. It looks a little like this:

Example
Example

例子: let bindings in do blocks.

main =
 do name <- getLine
    let loudName = makeLoud name
    putStrLn ("Hello " ++ loudName ++ "!")
    putStrLn ("Oh boy! Am I excited to meet you, " ++ loudName)


If you're paying attention, you might notice that the let binding above is missing an in. This is because let bindings in do blocks do not require the in keyword. You could very well use it, but then you'd have to make a mess of your do blocks. For what it's worth, the following two blocks of code are equivalent.

sweet unsweet
 do name <- getLine
    let loudName = makeLoud name
    putStrLn ("Hello " ++ loudName ++ "!")
    putStrLn ("Oh boy! Am I excited to meet you, " ++ loudName)
 do name <- getLine
    let loudName = makeLoud name
    in  do putStrLn ("Hello " ++ loudName ++ "!")
           putStrLn ("Oh boy! Am I excited to meet you, " ++ loudName)
练习
  1. Why does the unsweet version of the let binding require an extra do keyword?
  2. Do you always need the extra do?
  3. (extra credit) Curiously, let without in is exactly how we wrote things when we were playing with the interpreter in the beginning of this book. Why can you omit the in keyword in the interpreter, when you'd have to put it in when typing up a source file?




Learn more[编辑]

At this point, you should have the skills you need to do some fancier input/output. Here are some IO-related options to consider.

  • You could continue the sequential track, by learning more about types and eventually monads
  • Alternately: you could start learning about building graphical user interfaces in the GUI chapter
  • For more IO-related functionality, you could also consider learning more about the System.IO library



简单的输入输出
习题解答
Haskell基础

起步  >> 变量和函数  >> 列表和元组  >> 更进一步  >> 类型基础  >> 简单的输入输出  >> 类型声明


Haskell

Haskell基础 >> 初级Haskell >> Haskell进阶 >> Monads
高级Haskell >> 类型的乐趣 >> 理论提升 >> Haskell性能


库参考 >> 普通实务 >> 特殊任务