Haskell/类型基础

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

类型在编程中是一种分组相似值的方法。在Haskell中,类型系统是一种确保你的代码中存在更少错误的强有力的途径。

介绍[编辑]

编程处理不同的条目。例如,考虑把两个数相加:

2 + 3

2和3是什么?很清楚,它们是数。但是这个添加在中间的加号是什么?当然不是一个数。那它又是什么?

类似地,考虑一个程序问你的名字,然后说“Hello”。你的名字和Hello这个词都不是数字。它们是什么?我们应该提及所有的句子和段落等等作为文本。事实上,编程时使用的一个稍微更秘密的词“String”,要更普通。

Haskell中,规则是所有的类型名必须以一个大写字母开始。今后我们应该坚持这个转变。

如果你之前曾经设置过一个数据库,你将可能遇到过类型。例如,说我们在一个数据库中有一个表来存储一个人的详细联系信息;一种个人电话本。它的内容应该像这样:

电话号码 地址
743756 北京新天地小区22#1B
655523 上海蓝玛仁小区99#3B

字段中包含着值。“李”是一个值,“上海蓝玛仁小区99#3B”是一个值,“655523”也是一个值。如此说来,类型是用以分辨数据的标准。上表中有何类型?一二行的名和姓属于文字,所以这二个值的类型是String(字符串)。第三行是一个以名来定的值,电话号码。此值的类型是Number(数字)。

初看之下,地址行的值也是String类型的。然而事实上地址相当复杂。地址中有许多约定俗成的意义在。比如说,地址最后的一部分,通常是房间号,如果不是,也可能是信箱号,但总之是一个人的最详细地址。简单点说,此处的意义不止Text(文本)本身那点可怜的意义。我们可以说地址属于Text;这没什么关系。然而声明它属于另一种类型,比如Address,会更科学。如果我们知道某些数据时Text,这通常于事无补。然而若知道它属于Address,我们立刻就可以对它有更多解读。

这道理我们可以用在电话号码行上。故,TelephoneNumber这种类型产生了。以后我们要是再看到一串数字,只要知道是TelephoneNumber的类型,就会明白这不止是一串数字,而意会更多的信息。

不把TelephoneNumber看做Number(数字)类型的另一个原因是数字可以进行数学运算。那一个TelephoneNumber和1相加又有什么意义呐?电话可不会帮你转接张三的弟弟张四。因此把电话号码做成一种更强的类型而不仅仅是一个数字是有足够的理由的。而且,电话号码中的每个数字都是很重要的,不能多也不能少,所以没有四舍五入的说法,在电话号码前面加0就更是匪夷所思了。另一个理由是可能需要指定区号的位置,添加国家码。这样,最好的办法就是抽象出一个单独的类型。

为什么类型有用[编辑]

到现在,我们都好像只是在搞搞分类,几乎没有一点可以让现代计算机语言设计者吸收之处。别急,下一篇,我们会探究Haskell如何善用类型,使程序员获益。

使用交互式的:type命令[编辑]

字符和字符串[编辑]

在Haskell中,探寻类型如何发挥作用的最好方法是使用 GHCi 。运行之,来了解一下 :type 命令。

Example
Example

例子: 在GHCi中对字符使用 :t 命令

Prelude> :type 'H'
'H' :: Char


提示: :type 命令可缩写为 :t

这就是我们的使用方式。在 GHCi中,它会对一个值给出其类型。此例中,我们给了一个字符 'H' ——一个包在单引号里的字母 H ,GHCi 显示了它,其后跟着"::" ,也就是“类型是”的意思。整句话的意思是: 'H' 的类型是 Char 。

如果想给出一个字符串,要用双引号括起来。

Example
Example

例子: 在GHCi中对字符串使用 :t 命令

Prelude> :t "Hello World"
"Hello World" :: [Char]


这次,我们给出的是一个用双引号括起来的文本,GHCi的反应是: "Hello World" :: [Char] 。[Char] 意思是“字符构成的表”。注意区别 Char 和 [Char] ——带方括号的被用来构造文字列表。

练习
  1. 试用 :type 查询 "H" 的类型(注意双引号的使用),返回什么?为什么?.
  2. 试用 :type 查询 'Hello World' 的类型(注意单引号的使用),返回什么?为什么?.


在Haskell中字符串实质就是字符列表。在Haskell中可以用几种方法初始化字符串: 用双引号(ANSI 34)括起的连续的字符; 也可以像构建列表那样用":"将多个字符连接起来从而构成一个字符串,如 'H':'e':'l':'l':'o':[]; 还可以使用字符列表的形式来构建。

Haskell 有一个同义类的概念。就像英语里面的 'fast' 与 'quick', 两者意义相同,在Haskell中这种字面不同,但意义相同的两个类称为同义类(type synonyms)。就是说能使用 [Char]的地方一样能用 String来代替,如:

"Hello World" :: String

这样的表达也是正确的 。从这里开始我们将会更多的使用String,而不是[Char]来表示字符串。

布尔值[编辑]

另一种在其他语言中很常见的类型是布尔型(Boolean),或简称Bool。这是一种十分有用的类型。这种类型有两个值:True 或 False(对或错)。例如一个程序向使用者询问一个名字并在一个文件中查找这个名字相关项目。这时候如果我们有一个函数 nameExists用来确认这个名字是否存在是十分方便的。如果名字存在,可以用True来表达,如果名字不存在可以将结果表达为False。要注意的是在Haskell中布尔值是头字母大写的(其中的原因在逐步深入后会变得清晰)

Example
Example

例子: 在GHCI中探索 True 与 False的类型

Prelude> :t True
True :: Bool
Prelude> :t False
False :: Bool


这里就不用太多的解释了。 True 与 False 被归类为布尔型。

数值类型[编辑]

如果你已经使用:t对所有你已经熟悉的值进行查询,可能你会发现对5进行查询会返回如下的复杂类型。


Prelude> :t 5
5 :: (Num t) => t


简单来说,有很多数型(整数,小数等)而5可以属于不同的数型。这个看起来比较复杂的类型与Haskell的类型类特性有关。我们将会在以后的章节详细解释这种复杂的类型表示。

函数类型[编辑]

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[1]. Let's look at some examples.

之前我们展示了类型是如何应用在值(字符串,布尔值,字符,等)上的,可以看出Haskell中的类型不只是简单用于分分类,而且可用以描述值的特性。接着我们介绍,使类型系统真正强大的特性 -- 类型不只能应用在值上,还能应用在函数上 [2]。让我们看几个例子.

例子:not[编辑]

Example
Example

例子: 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:

not是一个标准的Prelude函数。功能就是把True变成False,False变成True。这里重新使用之前使用过的nameExists例子,重新定义一个相似的函数,但是这个函数是检验名字(not)是否"不"存在于电子数据表中。如下:

Example
Example

例子: nameDoesntExist: using not

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写一个类型标记我们关心两个方面:输入的值的类型,输出的值的类型。在我们的例子中,给not一个布尔型的值,然后返回一个布尔型的值。一次我们可以为not写出一下的类型标记:

Example
Example

例子: not的类型标记

not :: Bool -> Bool
注意: not是类型标记的一部分。


You can read this as 'not is a function from things of type Bool to things of type Bool'.

我们可以这样来描述这个类型标记: not函数取一个布尔值作为输入,返回一个布尔值。

例子:unlinesunwords[编辑]

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 unlines does. unwords is similar, but it uses a space instead of a newline as a separator. (mnemonic: un = unite)

程序设计中有一种任务很常见,这就是对一个字符串列表含有的每个元素添加换行符,再将它们连接成一个新的字符串。例如,对于列表 ["Bacon", "Sausages", "Egg"],我们希望把它合并为一个采购清单,对此,最直接的方法是,将列表中的每一项放入一个新行。这种方法就是unlinesunwords与它类似,区别在于后者用空格代替换行。(助记符: un = unite)

Example
Example

例子: unlines and unwords

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

请注意unlines的输出格式。虽与本章内容无关,我们将稍微解释一下这种格式。GHCi的所有输出,都首先利用show函数,此函数将输出转化为一个字符串。这是因为GHCi将把你命令的结果作为文本输出。如果输出已经是字符串了,show函数将做什么?毫无疑问,它什么都不会做;但具体行为上,与“什么都不会做”略有不同:字符串中出现的特殊字符,如tab,换行等,将被转义为\t,\n等格式。为了避免这种情况,可以使用putStrLn函数,这样将不会像平常一样默认调用show函数

Example
Example

例子: Using 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 show: putStrLn (show 5) (or use the equivalent function print: print 5).

第二个结果看起来似乎一致,但不要忘记它少了引号。putStrLn函数将严格输出你给它的东西(但实际上,putStrLn会给自己的输出自动加个换行符,putStr才会给出真正严格的输出)。同时,注意它将仅仅接受一个字符串作为参数。诸如putStrLn 5的函数调用将会失败,你必须将数字5转化为字符串才行,例如putStrLn (show 5) (或者print: print 5))

Getting back to the types. What would the types of unlines and 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.

现在让我们回到类型上来。unlinesunwords 是什么类型?观察它们的输入和输出,注意到这两个函数都将接受一个字符串列表,所以,它们的输入参数类型为[String]。处理并连接列表后,它们都将输出一个较长的字符串。所以,这两个函数的类型是 [String] -> String。注意,我们并未提及这两个函数用于连接的字符的不同,因为这对类型来说是微不足道的,它们都将输出一个字符串,含有换行的字符串的类型和含有空格的字符串的类型是一致的。

例子:chrord[编辑]

文字处理是计算机的一个问题. 当一切东西都到达最底层的时候, 计算机所知道的仅仅是1和0, 正如其在二进制下工作. 然而直接操作二进制并不方便, 人们开始让计算机保存文字信息. 每个字符应该先转换为数字, 然后再转换为二进制来存储. 因此, 文字, 或者说一串字符, 能够被编码为二进制. 一般来说, 我们只是关心字符如何用数字来表示, 因为再将数字转为二进制将会非常容易.


转换字元变成数字这件事是简单的,只要将所有可能的字元写下来,然后每个字元给一个数字。举例来说,我们可能给予字元 'a' 对应到 1, 字元 'b' 对应到2, 依此类推。这件事有一个称为ASCII标准已经帮我们做了,有128个标准常用的字元,数字都被编码在 ASCII 的表格里面。但是当我们每次需要用到一个字元时,都需要从表格中去把这些字元对应的数字找出来,或从数字中找出这些字元来,这真是一件无聊的事。所以,我们可以用两个函式来帮我们解决这个问题,chr(发音是 'char') 以及 ord

Example
Example

例子: Type signatures for chr and ord

chr :: Int  -> Char
ord :: Char -> Int


记得我们之前说过Haskell有多少数字类型么? 最简单的是Int类型, 它表示一个整数, to give them their proper name. [3] 记得上面类型标识么? 回忆一下上面的not是怎么工作的. 我们先是看到函数的参数类型, 然后是其返回类型. chr函数(返回对应数字编码的字符)的类型标识表示其接受一个Int类型的参数, 并返回一个Char. 执行反操作的是ord函数(返回对应字符的数字编码).

具体来说, 参考以下几个调用chrord的例子, 你可以看到这些类型是如何工作得. 注意这两个函数并不是内建函数, 而是在Data.Char模块中的, 因此你需要使用:m (:module的缩写)命令来加载之.

Example
Example

例子: Function calls to chr and ord

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?

到目前为止,我们只使用只有一个参数的函数。这太没趣了。例如,下面就是一个完全有效的Haskell函数,但是它是什么类型的呢?

Example
Example

例子: 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 x and y have to be Ints.

正如前面说的,数字可以表达为多种类型,只是我们在这里假装 x 和 y 必须是 Int 的。

There are very deep reasons for this, which we'll cover in the chapter on Currying.

这种做法有着深层次的原因。我们将在Currying一章中进行解读。

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:

对多参数函数来说最普遍的方式是把所有参数的类型都写在同一行,按字母排序(所以在这个例子中 x先于y),在它们中间插入->。最后,在行尾写上结果的类型并且在它前面加最后一个->。所以在这个例子中,我们有:

-->
  1. Write down the types of the arguments. We've already said that x and y have to be Ints, so it becomes:
    Int             Int
    ^^ x is an Int  ^^ y is an Int as well
    
  2. Fill in the gaps with ->:
    Int -> Int
  3. 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 

现实例子:openWindow[编辑]

A library is a collection of common code used by many programs.

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 [4]:

Example
Example

例子: openWindow

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?

  1. The negate function, which takes an Int and returns that Int with its sign swapped. For example, negate 4 = -4, and negate (-2) = 2
  2. The (&&) function, pronounced 'and', that takes two Bools and returns a third Bool which is True if both the arguments were, and False otherwise.
  3. The (||) function, pronounced 'or', that takes two Bools and returns a third Bool which is True if either of the arguments were, and False otherwise.

For any functions hereafter involving numbers, you can just assume the numbers are Ints.

  1. f x y = not x && y
  2. g x = (2*x - 1)^2
  3. h x y z = chr (x - 2)

多态性的类型[编辑]

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

到目前为止,我们已经看过一些具有单一型态的函式和值。然而,如果你开始在GHCi中玩 :t,你将会碰到一些型态的第一个字母不是熟悉的大写字母。举例来说,有一个函式用来找出列表的长度,称作 length. 记住,[Foo] 是一个存放型态Foo的事情的列表。然而我们希望length可以使用在存放任何型态的列表。而不是用计算存放整数的列表的长度用 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 使用型态变数来解决这个问题。例如:真实的型态长度如下:

Example
Example

例子: Our first polymorphic type

length :: [a] -> Int


We'll look at the theory behind polymorphism in much more detail later in the course.

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.

例子:fstsnd[编辑]

As we saw, you can use the fst and 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 fst and snd. First, a few sample calls to the functions:

Example
Example

例子: Example calls to fst and snd

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:

Example
Example

例子: The types of fst and snd

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:

Example
Example

例子: 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:

Example
Example

例子: 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 uppercase and 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 [5]. Let's look at some examples to see how the compiler works out types.

到目前为止,我们已经可以透过命令 :t 来看型态。然而,在你结束这章前,你正学习写一个完美的 Hasekell 程式码,这程式码已经可以被编译器接受。 换句话说,你不需要加上型别签章。如果你没有加上型别签章,这不代表 Hasekell 全部忽略型别这件事。相反地,当你没有告诉 HaseKell 你的函式或变数型别,Hasekell 会想办法生出来。这个流程叫做型别推论。借着它所知道的事情的型别,推论出其他事情的型别。型别推论对 Haskell来说是可决定性的,代表着编译器总是能够推论出型别,甚至你从没写过他们。


Example
Example

例子: 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:

Example
Example

例子: 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 [6]. (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 isL function.

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:

Example
Example

例子: 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.

类型防止错误发生[编辑]

假如你有以下几个函数:

Example
Example

例子: Type inference at work

fiveOrSix :: Bool -> Int
fiveOrSix True  = 5
fiveOrSix False = 6

pairToInt :: (Bool, String) -> Int
pairToInt x = fiveOrSix (fst x)


Our function 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:

Example
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:

Example
Example

例子: 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:

  1. f x y = uppercase (x ++ y)
  2. g (x,y) = fiveOrSix (isL x) - ord y
  3. h x y = pairToInt (fst x,y) + snd x + length y

提示[编辑]

  1. In fact, these are one and the same concept in Haskell.
  2. 事实上, 在Haskell中值跟函数是没有理论上的区别的.
  3. 其实Haskell拥有很多种整数类型! 不过不要担心, 我们将会在恰当的时候告诉你.
  4. This has been somewhat simplified to fit our purposes. Don't worry, the essence of the function is there.
  5. Some of the newer type system extensions to GHC do break this, however, so you're better off just always putting down types anyway.
  6. 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 (==) is Eq a => a -> a -> Bool.



类型基础
习题解答
Haskell基础

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


Haskell

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


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