Learn You a Haskell for Great Good! - Miran Lipovaca [57]
So now, when we implement a function that takes a name and a number and checks if that name and number combination is in our phonebook, we can give it a very pretty and descriptive type declaration.
inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> Bool
inPhoneBook name pnumber pbook = (name, pnumber) `elem` pbook
If we decided not to use type synonyms, our function would have this type:
inPhoneBook :: String -> String -> [(String, String)] -> Bool
In this case, the type declaration that takes advantage of type synonyms is easier to understand. However, you shouldn’t go overboard with these synonyms. We introduce type synonyms either to describe what some existing type represents in our functions (and thus our type declarations become better documentation) or when something has a longish type that’s repeated a lot (like [(String, String)]) but represents something more specific in the context of our functions.
Parameterizing Type Synonyms
Type synonyms can also be parameterized. If we want a type that represents an association list type, but still want it to be general so it can use any type as the keys and values, we can do this:
type AssocList k v = [(k, v)]
Now a function that gets the value by a key in an association list can have a type of (Eq k) => k -> AssocList k v -> Maybe v. AssocList is a type constructor that takes two types and produces a concrete type—for instance, AssocList Int String.
Just as we can partially apply functions to get new functions, we can partially apply type parameters and get new type constructors from them. When we call a function with too few parameters, we get back a new function. In the same way, we can specify a type constructor with too few type parameters and get back a partially applied type constructor. If we wanted a type that represents a map (from Data.Map) from integers to something, we could do this:
type IntMap v = Map Int v
Or we could do it like this:
type IntMap = Map Int
Either way, the IntMap type constructor takes one parameter, and that is the type of what the integers will point to.
If you’re going to try to implement this, you probably will want to do a qualified import of Data.Map. When you do a qualified import, type constructors also need to be preceded with a module name.
type IntMap = Map.Map Int
Make sure that you really understand the distinction between type constructors and value constructors. Just because we made a type synonym called IntMap or AssocList doesn’t mean that we can do stuff like AssocList [(1,2), (4,5),(7,9)]. All it means is that we can refer to its type by using different names. We can do [(1,2),(3,5),(8,9)] :: AssocList Int Int, which will make the numbers inside assume a type of Int. However, we can still use that list in the same way that we would use any normal list that has pairs of integers.
Type synonyms (and types generally) can be used only in the type portion of Haskell. Haskell’s type portion includes data and type declarations, as well as after a :: in type declarations or type annotations.
Go Left, Then Right
Another cool data type that takes two types as its parameters is the Either a b type. This is roughly how it’s defined:
data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)
It has two value constructors. If Left is used, then its contents are of type a; if Right is used, its contents are of type b. So we can use this type to encapsulate a value of one type or another. Then when we get a value of type Either a b, we usually pattern match on both Left and Right, and we do different stuff based on which one matches.
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
In this code, when we examine the type of Left True, we see that the type is Either Bool b. The first type parameter is Bool, because we made our value with the Left value constructor, whereas