Haskell/簡單的輸入輸出
迄今為止這本教材已經討論了返回數值的函數,它們很不錯。但是我們怎樣才能寫出"Hello world"?為了給你第一印象,這裡有一個Haskell版的"Hello world"程序:
例子: 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
的意義)。此外,當這個動作被「執行」(或是「運行」)時,結果的類型是()
。
註解
實際上這個類型說明 |
你現在可能已經猜到了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 |
Let's consider the following name program:
例子: 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.91Hint: 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
中取值是被容許的,但並非是必要的.我們可以這樣寫:
例子: 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):
例子: 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:
例子: 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 |
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:
例子: 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?
例子: 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, <-
.
例子: 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:
例子: 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 sThis 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 returnIO 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 IOmakeLoud
is certainly possible (how?), but missing the point entirely. - We could use
return
to promote the loud name into an action, writing something likeloudName <- return (makeLoud name)
. This is slightly better, in that we are at least leaving themakeLoud
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 anticlimaticreturn
- 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:
例子: 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) |
練習 |
---|
|
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
|