在前面我们提到在写类型名的时候,[Char]和String是等价的,可以互换。这就是由类型别名实现的。类型别名实际上什么也没做,只是给类型提供了不同的名字,让我们的代码更容易理解。这就是[Char]的别名String的由来。
type String = [Char]
我们已经介绍过了type关键字,这个关键字有一定误导性,它并不是用来创造新类(这是data关键字做的事情),而是给一个既有类型提供一个别名。
如果我们随便搞个函数toUpperString或其他什么名字,将一个字符串变成大写,可以用这样的类型声明toUpperString :: [Char] -> [Char], 也可以这样toUpperString :: String -> String,二者在本质上是完全相同的。后者要更易读些。
在前面Data.Map那部分,我们用了一个关联List来表示phoneBook,之后才改成的Map。我们已经发现了,一个关联List就是一组键值对组成的List。再看下我们phoneBook的样子:
phoneBook :: [(String,String)]
phoneBook =
[("betty","555-2938")
,("bonnie","452-2928")
,("patsy","493-2928")
,("lucille","205-2928")
,("wendy","939-8282")
,("penny","853-2492")
]
可以看出,phoneBook的类型就是[(String,String)],这表示一个关联List仅是String到String的映射关系。我们就弄个类型别名,好让它类型声明中能够表达更多信息。
type PhoneBook = [(String,String)]
现在我们phoneBook的类型声明就可以是phoneBook :: PhoneBook了。再给字符串加上别名:
type PhoneNumber = String
type Name = String
type PhoneBook = [(Name,PhoneNumber)]
Haskell程序员给String加别名是为了让函数中字符串的表达方式及用途更加明确。
好的,我们实现了一个函数,它可以取一名字和号码检查它是否存在于电话本。现在可以给它加一个相当好看明了的类型声明:
inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> Bool
inPhoneBook name pnumber pbook = (name,pnumber) `elem` pbook
如果不用类型别名,我们函数的类型声明就只能是String -> String -> [(String ,String)] -> Bool了。在这里使用类型别名是为了让类型声明更加易读,但你也不必拘泥于它。引入类型别名的动机既非单纯表示我们函数中的既有类型,也不是为了替换掉那些重复率高的长名字类型(如[(String,String)]),而是为了让类型对事物的描述更加明确。
类型别名也是可以有参数的,如果你想搞个类型来表示关联List,但依然要它保持通用,好让它可以使用任意类型作key和value,我们可以这样:
type AssocList k v = [(k,v)]
好的,现在一个从关联List中按键索值的函数类型可以定义为(Eq k) => k -> AssocList k v -> Maybe v. AssocList i。AssocList是个取两个类型做参数生成一个具体类型的类型构造子,如Assoc Int String等等。
Fronzie说:Hey!当我提到具体类型,那我就是说它是完全调用的,就像Map Int String。要不就是多态函数中的[a]或(Ord a) => Maybe a之类。有时我和孩子们会说“Maybe类型”,但我们的意思并不是按字面来,傻瓜都知道Maybe是类型构造子嘛。只要用一个明确的类型调用Maybe,如Maybe String,就可以得到一个具体类型。你知道,只有具体类型才可以储存值。
我们可以用不全调用来得到新的函数,同样也可以使用不全调用得到新的类型构造子。同函数一样,用不全的类型参数调用类型构造子就可以得到一个不全调用的类型构造子,如果我们要一个表示从整数到某东西间映射关系的类型,我们可以这样:
type IntMap v = Map Int v
也可以这样:
type IntMap = Map Int
无论怎样,IntMap的类型构造子都是取一个参数,而它就是这整数指向的类型。
Oh yeah,如果要你去实现它,很可能会用个qualified import来导入Data.Map。这时,类型构造子前面必须得加上模块名。所以应该写个type IntMap = Map.Map Int
你得保证真正弄明白了类型构造子和值构造子的区别。我们有了个叫IntMap或者AssocList的别名并不意味着我们可以执行类似AssocList [(1,2),(4,5),(7,9)]的代码,而是可以用不同的名字来表示原先的List,就像[(1,2),(4,5),(7,9)] :: AssocList Int Int让它里面的类型都是Int。而像处理普通的二元组构成的那种List处理它也是可以的。类型别名(类型依然不变),只可以在Haskell的类型部分中使用,像定义新类型或类型声明或类型注释中跟在::后面的部分。
另一个很酷的二参类型就是Either a b了,它大约是这样定义的:
data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)
它有两个值构造子。如果用了Left,那它内容的类型就是a;用了Right,那它内容的类型就是b。我们可以用它来将可能是两种类型的值封装起来,从里面取值时就同时提供Left和Right的模式匹配。
ghci> Right 20
Right 20
ghci> Left "w00t"
Left "w00t"
ghci> :t Right 'a'
Right 'a' :: Either a Char
ghci> :t Left True
Left True :: Either Bool b
到现在为止,Maybe是最常见的表示可能失败的计算的类型了。但有时Maybe也并不是十分的好用,因为Nothing中包含的信息还是太少。要是我们不关心函数失败的原因,它还是不错的。就像Data.Map的lookup只有在搜寻的项不在map时才会失败,对此我们一清二楚。但我们若想知道函数失败的原因,那还得使用Either a b,用a来表示可能的错误的类型,用b来表示一个成功运算的类型。从现在开始,错误一律用Left值构造子,而结果一律用Right。
一个例子:有个学校提供了不少壁橱,好给学生们地方放他们的Gun'N'Rose海报。每个壁橱都有个密码,哪个学生想用个壁橱,就告诉管理员壁橱的号码,管理员就会告诉他壁橱的密码。但如果这个壁橱已经让别人用了,管理员就不能告诉他密码了,得换一个壁橱。我们就用Data.Map的一个map来表示这些壁橱,把一个号码映射到一个表示壁橱占用情况及密码的二元组里。
import qualified Data.Map as Map
data LockerState = Taken | Free deriving (Show, Eq)
type Code = String
type LockerMap = Map.Map Int (LockerState, Code)
很简单,我们引入了一个新的类型来表示壁橱的占用情况。并为壁橱密码及按号码找壁橱的map分别设置了一个别名。好,现在我们实现这个按号码找壁橱的函数,就用Either String Code类型表示我们的结果,因为lookup可能会以两种原因失败。厨子已经让别人用了或者压根就没有这个橱子。如果lookup失败,就用字符串表明失败的原因。
lockerLookup :: Int -> LockerMap -> Either String Code
lockerLookup lockerNumber map =
case Map.lookup lockerNumber map of
Nothing -> Left $ "Locker number " ++ show lockerNumber ++ " doesn't exist!"
Just (state, code) -> if state /= Taken
then Right code
else Left $ "Locker " ++ show lockerNumber ++ " is already taken!"
我们在这里个map中执行一次普通的lookup,如果得到一个Nothing,就返回一个Left String的值,告诉他压根就没这个号码的橱子。如果找到了,就再检查下,看这橱子是不是已经让别人用了,如果是,就返回个Left String说它已经让别人用了。否则就返回个Right Code的值,通过它来告诉学生壁橱的密码。它实际上就是个Right String,我们引入了个类型别名让它这类型声明更好看。
如下是个map的例子:
lockers :: LockerMap
lockers = Map.fromList
[(100,(Taken,"ZD39I"))
,(101,(Free,"JAH3I"))
,(103,(Free,"IQSA9"))
,(105,(Free,"QOTSA"))
,(109,(Taken,"893JJ"))
,(110,(Taken,"99292"))
]
现在从里面lookup某个橱子号..
ghci> lockerLookup 101 lockers
Right "JAH3I"
ghci> lockerLookup 100 lockers
Left "Locker 100 is already taken!"
ghci> lockerLookup 102 lockers
Left "Locker number 102 doesn't exist!"
ghci> lockerLookup 110 lockers
Left "Locker 110 is already taken!"
ghci> lockerLookup 105 lockers
Right "QOTSA"
我们完全可以用Maybe a来表示它的结果,但这样一来我们就对得不到密码的原因不得而知了。而在这里,我们的新类型可以告诉我们失败的原因。
【本文翻译仅为外语学习及阅读目的,原文作者个人观点与译者及译言网无关】