2 + 3
在Haskell中，探寻类型如何发挥作用的最好方法是使用 GHCi 。运行之，来了解一下 :type 命令。
例子: 在GHCi中对字符使用 :t 命令
Prelude> :type 'H' 'H' :: Char
提示： :type 命令可缩写为
'H' ——一个包在单引号里的字母 H ，GHCi 显示了它，其后跟着"::" ，也就是“类型是”的意思。整句话的意思是： 'H' 的类型是 Char 。
例子: 在GHCi中对字符串使用 :t 命令
Prelude> :t "Hello World" "Hello World" :: [Char]
"Hello World" :: [Char] 。[Char] 意思是“字符构成的表”。注意区别 Char 和 [Char] ——带方括号的被用来构造文字列表。
在Haskell中字符串实质就是字符列表。在Haskell中可以用几种方法初始化字符串: 用双引号(ANSI 34)括起的连续的字符; 也可以像构建列表那样用":"将多个字符连接起来从而构成一个字符串，如
Haskell 有一个同义类的概念。就像英语里面的 'fast' 与 'quick', 两者意义相同，在Haskell中这种字面不同，但意义相同的两个类称为同义类(type synonyms)。就是说能使用
"Hello World" :: String
另一种在其他语言中很常见的类型是布尔型(Boolean)，或简称Bool。这是一种十分有用的类型。这种类型有两个值：True 或 False（对或错）。例如一个程序向使用者询问一个名字并在一个文件中查找这个名字相关项目。这时候如果我们有一个函数
例子: 在GHCI中探索 True 与 False的类型
Prelude> :t True True :: Bool Prelude> :t False False :: Bool
这里就不用太多的解释了。 True 与 False 被归类为布尔型。
Prelude> :t 5 5 :: (Num t) => t
So far, the types we have talked about apply to values (strings, booleans, characters, etc), and we have explained how types not only help to categorize them, but also describe them. The next thing we'll look at is what makes the type system truly powerful: We can assign types not only to values, but to functions as well. Let's look at some examples.
之前我们展示了类型是如何应用在值(字符串，布尔值，字符，等)上的，可以看出Haskell中的类型不只是简单用于分分类，而且可用以描述值的特性。接着我们介绍，使类型系统真正强大的特性 －－ 类型不只能应用在值上，还能应用在函数上 。让我们看几个例子.
例子: Negating booleans
not True = False not False = True
not is a standard Prelude function that simply negates Bools, in the sense that truth turns into falsity and vice versa. For example, given the above example we gave using Bools,
nameExists, we could define a similar function that would test whether a name doesn't exist in the spreadsheet. It would likely look something like this:
nameDoesntExist name = not (nameExists name)
To assign a type to
not we look at two things: the type of values it takes as its input, and the type of values it returns. In our example, things are easy.
not takes a Bool (the Bool to be negated), and returns a Bool (the negated Bool). Therefore, we write that:
not :: Bool -> Bool 注意: not是类型标记的一部分。
You can read this as '
not is a function from things of type Bool to things of type Bool'.
A common programming task is to take a list of Strings, then join them all up into a single string, but insert a newline character between each one, so they all end up on different lines. For example, say you had the list
["Bacon", "Sausages", "Egg"], and wanted to convert it to something resembling a shopping list, the natural thing to do would be to join the list together into a single string, placing each item from the list onto a new line. This is precisely what
unwords is similar, but it uses a space instead of a newline as a separator. (mnemonic: un = unite)
["Bacon", "Sausages", "Egg"]，我们希望把它合并为一个采购清单，对此，最直接的方法是，将列表中的每一项放入一个新行。这种方法就是
unwords与它类似，区别在于后者用空格代替换行。(助记符: un = unite)
Prelude> unlines ["Bacon", "Sausages", "Egg"] "Bacon\nSausages\nEgg\n" Prelude> unwords ["Bacon", "Sausages", "Egg"] "Bacon Sausages Egg"
Notice the weird output from
unlines. This isn't particularly related to types, but it's worth noting anyway, so we're going to digress a little and explore why this is. Basically, any output from GHCi is first run through the
show function, which converts it into a String. This makes sense, because GHCi shows you the result of your commands as text, so it has to be a String. However, what does
show do if you give it something which is already a String? Although the obvious answer would be 'do nothing', the behaviour is actually slightly different: any 'special characters', like tabs, newlines and so on in the String are converted to their 'escaped forms', which means that rather than a newline actually making the stuff following it appear on the next line, it is shown as "\n". To avoid this, we can use the
putStrLn function, which GHCi sees and doesn't run your output through
putStrLn in GHCi
Prelude> putStrLn (unlines ["Bacon", "Sausages", "Egg"]) Bacon Sausages Egg Prelude> putStrLn (unwords ["Bacon", "Sausages", "Egg"]) Bacon Sausages Egg
The second result may look identical, but notice the lack of quotes.
putStrLn outputs exactly what you give it (actually
putStrLn appends a newline character to its input before printing it; the function
putStr outputs exactly
what you give it). Also, note that you can only pass it a String. Calls like
putStrLn 5 will fail. You'd need to convert the number to a String first, that is, use
putStrLn (show 5) (or use the equivalent function
print: print 5).
putStrLn (show 5) (或者
print: print 5))
Getting back to the types. What would the types of
unwords be? Well, again, let's look at both what they take as an argument, and what they return. As we've just seen, we've been feeding these functions a list, and each of the items in the list has been a String. Therefore, the type of the argument is [String]. They join all these Strings together into one long String, so the return type has to be String. Therefore, both of the functions have type
[String] -> String. Note that we didn't mention the fact that the two functions use different separators. This is totally inconsequential when it comes to types — all that matters is that they return a String. The type of a String with some newlines is precisely the same as the type of a String with some spaces.
[String] -> String。注意，我们并未提及这两个函数用于连接的字符的不同，因为这对类型来说是微不足道的，它们都将输出一个字符串，含有换行的字符串的类型和含有空格的字符串的类型是一致的。
文字处理是计算机的一个问题. 当一切东西都到达最底层的时候, 计算机所知道的仅仅是1和0, 正如其在二进制下工作. 然而直接操作二进制并不方便, 人们开始让计算机保存文字信息. 每个字符应该先转换为数字, 然后再转换为二进制来存储. 因此, 文字, 或者说一串字符, 能够被编码为二进制. 一般来说, 我们只是关心字符如何用数字来表示, 因为再将数字转为二进制将会非常容易.
转换字元变成数字这件事是简单的，只要将所有可能的字元写下来，然后每个字元给一个数字。举例来说，我们可能给予字元 'a' 对应到 1, 字元 'b' 对应到2, 依此类推。这件事有一个称为ASCII标准已经帮我们做了，有128个标准常用的字元，数字都被编码在 ASCII 的表格里面。但是当我们每次需要用到一个字元时，都需要从表格中去把这些字元对应的数字找出来，或从数字中找出这些字元来，这真是一件无聊的事。所以，我们可以用两个函式来帮我们解决这个问题，
chr(发音是 'char') 以及
例子: Type signatures for
chr :: Int -> Char ord :: Char -> Int
Int类型, 它表示一个整数, to give them their proper name.  记得上面类型标识么? 回忆一下上面的
not是怎么工作的. 我们先是看到函数的参数类型, 然后是其返回类型.
ord的例子, 你可以看到这些类型是如何工作得. 注意这两个函数并不是内建函数, 而是在
例子: Function calls to
Prelude> :m Data.Char Prelude Data.Char> chr 97 'a' Prelude Data.Char> chr 98 'b' Prelude Data.Char> ord 'c' 99
So far, we've only worked with functions that take a single argument. This isn't very interesting! For example, the following is a perfectly valid Haskell function, but what would its type be?
例子: A function in more than one argument
f x y = x + 5 + 2 * y
As we've said a few times, there's more than one type for numbers, but we're going to cheat here and pretend that
y have to be Ints.
正如前面说的，数字可以表达为多种类型，只是我们在这里假装 x 和 y 必须是 Int 的。
The general technique for forming the type of a function in more than one argument, then, is to just write down all the types of the arguments in a row, in order (so in this case
x first then
y), then write
-> in between all of them. Finally, add the type of the result to the end of the row and stick a final
-> in just before it. So in this case, we have:
Write down the types of the arguments. We've already said that
yhave to be Ints, so it becomes:
Int Int ^^ x is an Int ^^ y is an Int as well
- Fill in the gaps with
Int -> Int
- Add in the result type and a final
->. In our case, we're just doing some basic arithmetic so the result remains an Int.
Int -> Int -> Int ^^ We're returning an Int ^^ There's the extra -> that got added in
As you'll learn in the Practical Haskell section of the course, one popular group of Haskell libraries are the GUI ones. These provide functions for dealing with all the parts of Windows or Linux you're familiar with: opening and closing application windows, moving the mouse around etc. One of the functions from one of these libraries is called
openWindow, and you can use it to open a new window in your application. For example, say you're writing a word processor like Microsoft Word, and the user has clicked on the 'Options' button. You need to open a new window which contains all the options that they can change. Let's look at the type signature for this function :
openWindow :: WindowTitle -> WindowSize -> Window
Don't panic! Here are a few more types you haven't come across yet. But don't worry, they're quite simple. All three of the types there, WindowTitle, WindowSize and Window are defined by the GUI library that provides
openWindow. As we saw when constructing the types above, because there are two arrows, the first two types are the types of the parameters, and the last is the type of the result. WindowTitle holds the title of the window (what appears in the blue bar (XP and before) or black translucent bar (Vista) - you didn't change the color, did you? - at the top), WindowSize how big the window should be. The function then returns a value of type Window which you can use to get information on and manipulate the window.
Finding types for functions is a basic Haskell skill that you should become very familiar with. What are the types of the following functions?
For any functions hereafter involving numbers, you can just assume the numbers are Ints.
So far all we've looked at are functions and values with a single type. However, if you start playing around with :t in GHCi you'll quickly run into things that don't have types beginning with the familiar capital letter. For example, there's a function that finds the length of a list, called (rather predictably)
length. Remember that [Foo] is a list of things of type Foo. However, we'd like
length to work on lists of any type. I.e. we'd rather not have a
lengthInts :: [Int] -> Int, as well as a
lengthBools :: [Bool] -> Int, as well as a
lengthStrings :: [String] -> Int, as well as a...
length. 记住，[Foo] 是一个存放型态Foo的事情的列表。然而我们希望
lengthInts :: [Int] -> Int，计算存放布尔值的列表长度用
lengthBools :: [Bool] -> Int，计算存放字串列表的长度用
lengthStrings :: [String] -> Int, 等等。
That's too complicated. We want one single function that will find the length of any type of list. The way Haskell does this is using type variables. For example, the actual type of length is as follows:
这太复杂了，我们想要有一个单一的函式，可以计算出每一种存放所有型态列表的长度，所以， Haskell 使用型态变数来解决这个问题。例如：真实的型态长度如下：
例子: Our first polymorphic type
length :: [a] -> Int
The "a" you see there in the square brackets is called a type variable. Type variables begin with a lowercase letter. Indeed, this is why types have to begin with an uppercase letter — so they can be distinguished from type variables. When Haskell sees a type variable, it allows any type to take its place. This is exactly what we want. In type theory (a branch of mathematics), this is called polymorphism: functions or values with only a single type (like all the ones we've looked at so far except
length) are called monomorphic, and things that use type variables to admit more than one type are therefore polymorphic.
As we saw, you can use the
snd functions to extract parts of pairs. By this time you should be in the habit of thinking "What type is that function?" about every function you come across. Let's examine
snd. First, a few sample calls to the functions:
例子: Example calls to
Prelude> fst (1, 2) 1 Prelude> fst ("Hello", False) "Hello" Prelude> snd (("Hello", False), 4) 4
To begin with, let's point out the obvious: these two functions take a pair as their parameter and return one part of this pair. The important thing about pairs, and indeed tuples in general, is that they don't have to be homogeneous with respect to types; their different parts can be different types. Indeed, that is the case in the second and third examples above. If we were to say:
fst :: (a, a) -> a
That would force the first and second part of input pair to be the same type. That illustrates an important aspect to type variables: although they can be replaced with any type, they have to be replaced with the same type everywhere. So what's the correct type? Simply:
例子: The types of
fst :: (a, b) -> a snd :: (a, b) -> b
Note that if you were just given the type signatures, you might guess that they return the first and second parts of a pair, respectively. In fact this is not necessarily true, they just have to return something with the same type of the first and second parts of the pair.
Now we've explored the basic theory behind types and types in Haskell, let's look at how they appear in code. Most Haskell programmers will annotate every function they write with its associated type. That is, you might be writing a module that looks something like this:
例子: Module without type signatures
module StringManip where import Data.Char uppercase = map toUpper lowercase = map toLower capitalise x = let capWord  =  capWord (x:xs) = toUpper x : xs in unwords (map capWord (words x))
This is a small library that provides some frequently used string manipulation functions.
uppercase converts a string to uppercase,
lowercase to lowercase, and
capitalize capitalizes the first letter of every word. Providing a type for these functions makes it more obvious what they do. For example, most Haskellers would write the above module something like the following:
例子: Module with type signatures
module StringManip where import Data.Char uppercase, lowercase :: String -> String uppercase = map toUpper lowercase = map toLower capitalise :: String -> String capitalise x = let capWord  =  capWord (x:xs) = toUpper x : xs in unwords (map capWord (words x))
Note that you can group type signatures together into a single type signature (like ours for
lowercase above) if the two functions share the same type.
So far, we've explored types by using the :t command in GHCi. However, before you came across this chapter, you were still managing to write perfectly good Haskell code, and it has been accepted by the compiler. In other words, it's not necessary to add type signatures. However, if you don't add type signatures, that doesn't mean Haskell simply forgets about typing altogether! Indeed, when you didn't tell Haskell the types of your functions and variables, it worked them out. This is a process called type inference, whereby the compiler starts with the types of things it knows, then works out the types of the rest of the things. Type inference for Haskell is decidable, which means that the compiler can always work out the types, even if you never write them in . Let's look at some examples to see how the compiler works out types.
到目前为止，我们已经可以透过命令 :t 来看型态。然而，在你结束这章前，你正学习写一个完美的 Hasekell 程式码，这程式码已经可以被编译器接受。 换句话说，你不需要加上型别签章。如果你没有加上型别签章，这不代表 Hasekell 全部忽略型别这件事。相反地，当你没有告诉 HaseKell 你的函式或变数型别，Hasekell 会想办法生出来。这个流程叫做型别推论。借着它所知道的事情的型别，推论出其他事情的型别。型别推论对 Haskell来说是可决定性的，代表着编译器总是能够推论出型别，甚至你从没写过他们。
例子: Simple type inference
-- We're deliberately not providing a type signature for this function -- 我们故意不提供这个函数的类型指纹. isL c = c == 'l'
This function takes a character and sees if it is an 'l' character. The compiler derives the type for
isL something like the following:
例子: A typing derivation
(==) :: a -> a -> Bool 'l' :: Char Replacing the second ''a'' in the signature for (==) with the type of 'l': (==) :: Char -> Char -> Bool isL :: Char -> Bool
The first line indicates that the type of the function
(==), which tests for equality, is
a -> a -> Bool . (We include the function name in parentheses because it's an operator: its name consists only of non-alphanumeric characters. More on this later.) The compiler also knows that something in 'single quotes' has type Char, so clearly the literal 'l' has type Char. Next, the compiler starts replacing the type variables in the signature for
(==) with the types it knows. Note that in one step, we went from
a -> a -> Bool to
Char -> Char -> Bool, because the type variable
a was used in both the first and second argument, so they need to be the same. And so we arrive at a function that takes a single argument (whose type we don't know yet, but hold on!) and applies it as the first argument to
(==). We have a particular instance of the polymorphic type of
(==), that is, here, we're talking about
(==) :: Char -> Char -> Bool because we know that we're comparing Chars. Therefore, as
(==) :: Char -> Char -> Bool and we're feeding the parameter into the first argument to
(==), we know that the parameter has the type of Char. Phew!
But wait, we're not finished yet! What's the return type of the function? Thankfully, this bit is a bit easier. We've fed two Chars into a function which (in this case) has type
Char -> Char -> Bool, so we must have a Bool. Note that the return value from the call to
(==) becomes the return value of our
So, let's put it all together.
isL is a function which takes a single argument. We discovered that this argument must be of type Char. Finally, we derived that we return a Bool. So, we can confidently say that
isL has the type:
isL with a type
isL :: Char -> Bool isL c = c == 'l'
And, indeed, if you miss out the type signature, the Haskell compiler will discover this on its own, using exactly the same method we've just run through.
So if type signatures are optional, why bother with them at all? Here are a few reasons:
- Documentation: the most prominent reason is that it makes your code easier to read. With most functions, the name of the function along with the type of the function is sufficient to guess at what the function does. (Of course, you should always comment your code anyway.)
- Debugging: if you annotate a function with a type, then make a typo in the body of the function, the compiler will tell you at compile-time that your function is wrong. Leaving off the type signature could have the effect of allowing your function to compile, and the compiler would assign it an erroneous type. You wouldn't know until you ran your program that it was wrong. In fact, this is so important, let's explore it some more.
例子: Type inference at work
fiveOrSix :: Bool -> Int fiveOrSix True = 5 fiveOrSix False = 6 pairToInt :: (Bool, String) -> Int pairToInt x = fiveOrSix (fst x)
fiveOrSix takes a Bool. When
pairToInt receives its arguments, it knows, because of the type signature we've annotated it with, that the first element of the pair is a Bool. So, we could extract this using
fst and pass that into
fiveOrSix, and this would work, because the type of the first element of the pair and the type of the argument to
fiveOrSix are the same.
This is really central to typed languages. When passing expressions around you have to make sure the types match up like they did here. If they don't, you'll get type errors when you try to compile; your program won't typecheck. This is really how types help you to keep your programs bug-free. To take a very trivial example:
例子: A non-typechecking program
"hello" + " world"
Having that line as part of your program will make it fail to compile, because you can't add two strings together! More likely, you wanted to use the string concatenation operator, which joins two strings together into a single one:
例子: Our erroneous program, fixed
"hello" ++ " world"
An easy typo to make, but because you use Haskell, it was caught when you tried to compile. You didn't have to wait until you ran the program for the bug to become apparent.
This was only a simple example. However, the idea of types being a system to catch mistakes works on a much larger scale too. In general, when you make a change to your program, you'll change the type of one of the elements. If this change isn't something that you intended, then it will show up immediately. A lot of Haskell programmers remark that once they have fixed all the type errors in their programs, and their programs compile, that they tend to 'just work': function flawlessly first time, with only minor problems. Run-time errors, where your program goes wrong when you run it rather than when you compile it, are much rarer in Haskell than in other languages. This is a huge advantage of a strong type system like Haskell's.
Infer the types of following functions:
- ↑ In fact, these are one and the same concept in Haskell.
- ↑ 事实上, 在Haskell中值跟函数是没有理论上的区别的.
- ↑ 其实Haskell拥有很多种整数类型! 不过不要担心, 我们将会在恰当的时候告诉你.
- ↑ This has been somewhat simplified to fit our purposes. Don't worry, the essence of the function is there.
- ↑ Some of the newer type system extensions to GHC do break this, however, so you're better off just always putting down types anyway.
- ↑ This is a slight lie. That type signature would mean that you can compare two values of any type whatsoever, but this clearly isn't true: how can you see if two functions are equal? Haskell includes a kind of 'restricted polymorphism' that allows type variables to range over some, but not all types. Haskell implements this using type classes, which we'll learn about later. In this case, the correct type of
Eq a => a -> a -> Bool.
起步 >> 变量和函数 >> 列表和元组 >> 更进一步 >> 类型基础 >> 简单的输入输出 >> 类型声明