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.
起步 >> 變量和函數 >> 列表和元組 >> 更進一步 >> 類型基礎 >> 簡單的輸入輸出 >> 類型聲明