「学习笔记」Chapter 7 - Part I Making Our Own Types And Type Classes
In this chapter, you’ll learn how to create custom types and put them to work!
One way to make our own type is to use the data keyword.In the standard library, Bool type is defined as
data Bool = True | False
The part before the equal sign denotes the type, which in this case is Bool. The parts after the equal sign are value constructors. They specify the different values that this type can have. The | is read as or.
In this section,we define a new type called Shape
data Shape = Circle Float Float Float | Rectangle Float Float Float Float
The Circle value constructor has three fields, which take floats. So when we write a value constructor, we can optionally add some types after it, and those types define the types of values it will contain
Let's look at type of Circle , Rectangle , and a Shape value
ghci>:t Circle Circle :: Float -> Float -> Float -> Shapeghci>:t Rectangle Rectangle :: Float -> Float -> Float -> Float -> Shapeghci>let p = Circle 1.1 2.3 3.1ghci>:t pp :: Shape
From the result above, we find that Circle, as a value construct, looks like a function: It takes three Float parameter, then return a Shape value.Then we can also map it to a list.
ghci>map (Circle 1 2) [4,5,6][Circle 1.0 2.0 4.0,Circle 1.0 2.0 5.0,Circle 1.0 2.0 6.0]
Now, we define a function which takes a Shape and return it's area.
area :: Shape -> Floatarea (Circle _ _ r) = pi * r^2area (Rectangle x1 y1 x2 y2) = (abs $ x1 - x2) * (abs $ y1 - y2)
Note that Circle is not a type, so you can't write "Circle -> Float". In area, we use pattern matching to deal with different type of Shape.
Look at the test result
ghci>area (Circle 1 2 3)28.274334ghci>area (Rectangle 1 2 3 4)4.0
If you type "Circle 1 2 3" in ghci, it will return an error, as ghci doesn't know how to display the type Shape, so we must tell ghci how to display the type.
Define type Shape as following,
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)
Then look at the test result.
ghci>Circle 1 2 3Circle 1.0 2.0 3.0
We won’t concern ourselves with deriving too much for now. Let’s just say that if we add deriving (Show) at the end of a data declaration , Haskell automatically makes that type part of the Show type class.
Let’s make an intermediate data type that defines a point in two-dimensional space. Then we can use that to make our shapes more understandable.
data Point = Point Float Float deriving (Show)data Shape = Circle Point Float | Rectangle Point Point deriving (Show)
After that, we must redefine our function area
area :: Shape -> Floatarea (Circle _ r) = pi * r^2area (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x1 - x2) * (abs $ y1 - y2)
Let's look at the test result
ghci>area ( Rectangle (Point 1 2) (Point 3 4) )4.0ghci>area (Circle (Point 1 2) 3)28.274334
We can also move the shape to another position.
move :: Shape -> Float -> Float -> Shapemove (Circle (Point x y) r) a b = ( Circle (Point (x+a) (y+b)) r )move (Rectangle (Point x1 y1) (Point x2 y2)) a b = ( Rectangle (Point (x1+a) (y1+b)) (Point (x2+a) (y2+b)) )
Let's look at the test result
ghci>move (Circle (Point 0 0) 3) 1 2Circle (Point 1.0 2.0) 3.0
We can add the following code at the beginnig of the Module file.
module Shapes ( Point (..) , Shape (..) , area , move ) where(..): It means that we will export all the value constructor in the type.where: Don't forget the keywords whereNote: Note that we can only export some of the value constructor. In this way, we hide the details of the type.Provide abstraction and constrains user's operation on the type.
Say we’ve been tasked with creating a data type that describes a person. The information that we want to store about that person is first name, last name, age, height,phone number, and favorite ice cream flavor.
Let's define the person's information as before.
data Person = Person String String Int Float deriving (Show)
Then we test this type.
ghci>:t PersonPerson :: String -> String -> Int -> Float -> Personghci>Person "Jim" "Kong" 23 175Person "Jim" "Kong" 23 175.0
Now, We want to get information of a person, then we can define some functions to do it.
getFirstName :: Person -> StringgetFirstName (Person firstName _ _ _) = firstNamegetSecondName :: Person -> StringgetSecondName (Person _ secondName _ _ ) = secondNamegetAge :: Person -> IntgetAge (Person _ _ age _ ) = age
Let's test them.
ghci>getFirstName p"Kim"ghci>getSecondName p"Kong"ghci>getAge p23
The function defined above are all in the same mode, it fileds of a person's information is too large, we'll be boring to write such code. Haskell gives us an alternative way to write data types. Here’s how we could achieve the same functionality with record syntax:
data Person' = Person' { firstName :: String, secondName :: String, age :: Int, weight :: Float } deriving (Show)Let's look at type of this new type
ghci>:t Person'Person' :: String -> String -> Int -> Float -> Person'ghci>Person' "Kim" "Kong" 23 175Person' {firstName = "Kim", secondName = "Kong", age = 23, weight = 175.0}ghci>:t firstName firstName :: Person' -> StringFrom the result above, we can find that firstName is a function.We test it again.
ghci>let p = (Person' "Kim" "Kong" 23 175)ghci>pPerson' {firstName = "Kim", secondName = "Kong", age = 23, weight = 175.0}ghci>firstName p"Kim"ghci>secondName p"Kong"In fact, define a type like the way of *Person'*,Haskell automatically makes these functions: firstName, lastName, age, height for us.
A value constructor can take some parameters and then produce a new value.In a similar manner, type constructors can take types as parame- ters to produce new types.
Let’s take a look at how a type we’ve already met is implemented.
data Maybe a = Nothing | Just a
Here, Maybe is a type constructors, a is a type parameter. If a is Int, then we have a type Just Int.No value can have a type of Maybe, because that’s not a type,it’s a type constructor.
When does using type parameters make sense? Usually, we use them when our data type would work regardless of the type of the value it then holds, as with our Maybe a type. We usually use type parameters when the type that’s contained inside the data type’s various value constructors isn’t really that important for the type to work. it’s a very strong convention in Haskell to never add type class constraints in data declarations. Why? Well, because it doesn’t provide much benefit, and we end up writing more class constraints, even when we don’t need them. Don’t put type constraints into data declarations, even if it seems to make sense. You’ll need to put them into the function type declarations ei- ther way.
Let’s implement a 3D vector type and add some operations for it. We’ll make it a parameterized type, because even though it will usually contain numeric types, it will still support several of them, like Int, Integer, and Double.
data Vector a = Vector a a a deriving (Show)-- a can be Int, Float, Double, ... , anything of type Numvplus :: (Num a) => Vector a -> Vector a -> Vector a(Vector i j k ) `vplus` (Vector l m n ) = (Vector (i+l) (j+m) (k+n))vmul :: (Num a) => Vector a -> Vector a -> Vector a(Vector i j k ) `vmul` (Vector l m n ) = (Vector (i*l) (j*m) (k*n))
Notice that we didn’t put a Num class constraint in the data declaration. As explained in the previous section, even if we put it there, we would still need to repeat it in the functions.
Once again, it’s very important to distinguish between the type construc- tor and the value constructor. When declaring a data type, the part before the = is the type constructor(Vecor a), and the constructors after it (possibly separated by | characters)(Vector a a a) are value constructors.
For instance, giving a function the following type would be wrong:
Vector a a a -> Vector a a a -> a
This doesn’t work because the type of our vector is Vector a, and not Vector a a a.
Now, let's test our Vector
ghci>(Vector 1 2 3) `vplus` (Vector 4 5 6)Vector 5 7 9ghci>(Vector 1.1 2.2 3.3) `vplus` (Vector 4.4 5.5 6.6)Vector 5.5 7.7 9.899999999999999
From the test above, we can see that vplus can work on (Vector Int) and (Vector Double) at the same time.
In “Type Classes 101” , you learned that a type class is a sort of an interface that defines some behavior, and that a type can be made an instance of a type class if it supports that behavior. For example, the Int type is an instance of the Eq type class because the Eq type class defines behavior for stuff that can be equated. And because integers can be equated, Int was made a part of the Eq type class. The real usefulness comes with the functions that act as the interface for Eq, namely == and /=. If a type is a part of the Eq type class, we can use the == functions with values of that type. In Haskell, we first make our data type, and then we think about how it can act. If it can act like something that can be equated, we make it an instance of the Eq type class. If it can act like something that can be ordered, we make it an instance of the Ord type class. Let’s see how Haskell can automatically make our type an instance of any of the following type classes: Eq, Ord, Enum, Bounded, Show, and Read.
Let's consider the type
data Person'' = Person'' { firstName :: String, secondName :: String, age :: Int, } If we want to compare whether two people are equal or not, we should define it as
data Person'' = Person'' { firstName'' :: String, secondName'' :: String, age'' :: Int, } deriving (Eq)When we derive the Eq instance for a type and then try to compare two values of that type with == or /=,Haskell will
Let's test the type
ghci>let p = (Person'' "Kim" "Kong" 10)ghci>let q = (Person'' "Kim" "KongII" 2)ghci>p == qFalseghci>p /= qTrue
The Show and Read type classes are for things that can be converted to or from strings, respectively. As with Eq, if a type’s constructors have fields, their type must be a part of Show or Read if we want to make our type an instance of them. Let’s make our Person data type a part of Show and Read as well.
data Person3 = Person3 { firstName3 :: String, secondName3 :: String, age3 :: Int } deriving (Eq,Read,Show)Let's see the test result
ghci>let p = (Person3 "Ha" "Hia" 2)ghci>pPerson3 {firstName3 = "Ha", secondName3 = "Hia", age3 = 2}ghci>show p"Person3 {firstName3 = \"Ha\", secondName3 = \"Hia\", age3 = 2}"Read is pretty much the inverse type class of Show. It’s for converting strings to values of our type. Remember though, that when we use the read function, we might need to use an explicit type annotation to tell Haskell which type we want to get as a result.
ghci>let q = "Person3 {firstName3 = \"Ha\", secondName3 = \"Hia\", age3 = 2}"ghci>q"Person3 {firstName3 = \"Ha\", secondName3 = \"Hia\", age3 = 2}"ghci>let q' = read q :: Person3ghci>q'Person3 {firstName3 = "Ha", secondName3 = "Hia", age3 = 2}"read q :: Person3" tells ghci to read a string q as type Person3.
We can derive instances for the Ord type class, which is for types that have values that can be ordered. We define a new type two demonstrate Ord
data MyInt = Int2 | Int1 | Int3 deriving (Eq,Ord)
Let's test it.
ghci>Int1 `compare` Int2GTghci>Int2 `compare` Int3LTghci>Int1 `compare` Int3LT
We can see that Int1 is greater than Int2, but less than Int3. This is because we define them in the order (Int2, Int1, Int3).
Consider the following type:
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday
Because all the type’s value constructors are nullary (that is, they don’t have any fields), we can make it part of the Enum type class. The Enum type class is for things that have predecessors and successors. We can also make it part of the Boundedtype class, which is for things that have a lowest possible value and highest possible value.
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Eq, Ord, Show, Read, Bounded, Enum)
Now, let's test this type to see what we can do on it.
ghci>Wednesday Wednesdayghci>show Wednesday "Wednesday"ghci>Saturday == Saturday Trueghci>Saturday > Monday Trueghci>Monday `compare` Friday LT
As it is part of Bounded, we can use it as
ghci>minBound :: DayMondayghci>maxBound :: DaySunday
As it is part of Enum, we can use it as
ghci>succ Monday Tuesdayghci>pred Tuesday Mondayghci>pred Monday error
Type synonyms don’t really do anything,they’re just about giving some types different names so that they make more sense to someone reading our code and documentation.
Here’s how the standard library defines String as a synonym for [Char]
type String = [Char]
Our original version of Phonebook is like:
phonebook :: [(String, String)]phoneBook =[("betty", "555-2938"),("bonnie", "452-2928"),("patsy", "493-2928"),("lucille", "205-2928"),("wendy", "939-8282"),("penny", "853-2492")]Now, let's make it more readable.
type Name = Stringtype PhoneNumber = Stringtype PhoneBook = [(Name, PhoneNumber)]phoneBook =[("betty", "555-2938"),("bonnie", "452-2928"),("patsy", "493-2928"),("lucille", "205-2928"),("wendy", "939-8282"),("penny", "853-2492")]After that, we can define a function which is more descriptive.
isInPB :: Name -> PhoneNumber -> PhoneBook -> BoolisInPB name pn pb = (name, pn) `elem` pb
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 AssoList k v = [(k,v)]mIntStrPair :: AssoList Int StringmIntStrPair = [(1, "1") ,(2, "2") ]
Let's test it!
ghci>:l chap7.hs[1 of 1] Compiling Shapes ( chap7.hs, interpreted )Ok, modules loaded: Shapes.ghci>:t mIntStrPair mIntStrPair :: AssoList Int String
We load the test file successfully and get type of mIntStrPair.
Just as we can partially apply functions to get new functions, we can par- tially apply type parameters and get new type constructors from them. If we wanted a type that represents a map (from Data.Map) from integers to something, we could do this:
import qualified Data.Map as Maptype IntMap v = Map.Map Int vmMapIntString :: IntMap StringmMapIntString = Map.fromList [(1,"ha"),(2,"ha"),(3,"pia")]
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
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.Let's test it in ghci.
ghci>Left 3Left 3ghci>Right 4Right 4ghci>Right "h"Right "h"ghci>:t (Left 3)(Left 3) :: Num a => Either a bghci>:t (Right 4)(Right 4) :: Num b => Either a bghci>:t (Right "h")(Right "h") :: Either a [Char]
Why Either is useful Maybe a isn’t good enough,because Nothing doesn’t convey much information other than that something has failed. When we’re interested in how or why some function failed, we usually use the result type of Either a b, where a is a type that can tell us something about the possible failure, and b is the type of a successful computation. Hence, errors use the Left value constructor, and results use Right.
We give an example.
myDiv :: Float -> Float -> Either Float StringmyDiv a b = case b of 0 -> Right "divided by 0" otherwise -> Left $ a/b
Test it in GHCI.
ghci>myDiv 4 3Left 1.3333334ghci>myDiv 4 0Right "divided by 0"