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
|