跳至內容

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性能


庫參考 >> 普通實務 >> 特殊任務