跳至內容

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


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