Algebraic Data Types
In the previous chapter, we talked about some built-in Haskell types and typeclass. In this chapter, we will learn the construction types and typeclass methods.
We have seen many types, suchBool
,Int
,Char
,Maybe
Wait, but in Haskell, how should we construct our own types? Good question. One way is to useDataKeyword. Let's take a look.Bool
Definition in the Standard Library:
data Bool = False | True
DataTo define a new type.=
The left endBool
,=
IsValue Constructor(Value Constructor), They specify the possible values of this type.|
Read this statement as "or ".:Bool
The type value can beTrue
OrFalse
. The first letter of the type name and value constructor must be capitalized.
Similar, we can imagineInt
Type Declaration:
data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | ... | 2147483647
Both the header and end values are represented by the constructor.Int
The minimum and maximum values of the type. Note that the true type declaration is not long. This is only for ease of understanding. We use ellipsis to represent a large number omitted in the middle.
Let's look at the graphical representation in Haskell. Indicates that a circle can use a tuple, such(43.1,55.0,10.4)
, The first two items indicate the center position, and the last point indicates the radius. It sounds good, but 3d vectors or anything else may be in this form! A better way is to construct a chart type by yourself. Assume that the image can be a circle or a rectangle ):
data Shape = Circle Float Float Float | Rectangle Float Float Float Float
What is this?Circle
The value constructor of has three items, all of which are float. We can see that when defining the value structure, we can follow several types to indicate the types of values it contains. Here, the first two items represent the coordinates of the center, and the last item represents the radius.Rectangle
The value constructor of takes fourFloat
Item. The first two items represent the coordinates in the upper left corner, and the last two items represent the coordinates in the lower right corner.
When talking about "field", it should actually be "Parameters ). A value constructor is essentially a function that returns a value of the type. Let's take a look at the two values to construct the sub-type declaration:
ghci> :t Circle
Circle :: Float -> Float -> Float -> Shape
ghci> :t Rectangle
Rectangle :: Float -> Float -> Float -> Float -> Shape
Cool. In this case, the value constructor is no different from a common function. Who wants it? Let's write a function to calculate the graphic area:
surface :: Shape -> Float
surface (Circle _ _ r) = pi * r ^ 2
surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)
It is worth mentioning that its type declaration indicates that the function obtainsShape
And returnsFloat
Value. WriteCircle -> Float
No, becauseCircle
Not a type. The actual type should beShape
. This cannot be written.True->False
The truth is the same. Then, the pattern matching we use is for the value constructor. We have previously matched[]
,False
Or5
They are all constructors that do not contain parameter values.
We only care about the radius of the circle, so we do not need to care about the first two items of the coordinate:
ghci> surface $ Circle 10 20 10
314.15927
ghci> surface $ Rectangle 0 0 100 100
10000.0
Yay, it works! However, if we try to outputCircle 10 20
Go to the console and you will get an error. This is because Haskell does not know the string representation method of this type. Think about it. When we output a value to the console, Haskell will first callshow
The function outputs the string representation of this value. Therefore, let usShape
Type becomes a member of the show type class. You can modify it as follows:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)
Do not go into details firstDeriving(Derived), you can first understand:data
Addderiving (Show)
Then Haskell will automaticallyShow
Type class. GoodBecause the value constructor is a function, we can give itmap
, Using it for all calls, and everything that common functions can do.
ghci> Circle 10 20 5
Circle 10.0 20.0 5.0
ghci> Rectangle 50 230 60 90
Rectangle 50.0 230.0 60.0 90.0
If we want to take a group of concentric circles with different radius, we can do this:
ghci> map (Circle 10 20) [4,5,6,6]
[Circle 10.0 20.0 4.0,Circle 10.0 20.0 5.0,Circle 10.0 20.0 6.0,Circle 10.0 20.0 6.0]
Our types can also be better. Add a type that represents the midpoint of a two-dimensional space.Shape
Easier to understand:
data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float | Rectangle Point Point deriving (Show)
Note:Point
Its type and value constructor use the same name. There is no special meaning. In fact, this type of name duplication is very common when a type contains unique value constructor. Okay, now ourCircle
Contains two items, one isPoint
Type, one isFloat
Type.Rectangle
Similarly, we have to modifysurface
Function to adapt to changes in the type definition.
surface :: Shape -> Float
surface (Circle _ r) = pi * r ^ 2
surface (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1)
The only thing that needs to be modified is the mode. InCircle
We ignore the entirePoint
. InRectangle
We use a nested mode to obtainPoint
. For some reasonPoint
.
ghci> surface (Rectangle (Point 0 0) (Point 100 100))
10000.0
ghci> surface (Circle (Point 0 0) 24)
1809.5574
How do I write a function that indicates moving a graph? It should takeShape
And represent the two numbers of displacement, return a new position in the image.
nudge :: Shape -> Float -> Float -> Shape
nudge (Circle (Point x y) r) a b = Circle (Point (x+a) (y+b)) r
nudge (Rectangle (Point x1 y1) (Point x2 y2)) a b = Rectangle (Point (x1+a) (y1+b)) (Point (x2+a) (y2+b))
Concise and clear. Let's give this one more.Shape
Point plus the amount of displacement.
ghci> nudge (Circle (Point 34 34) 10) 5 10
Circle (Point 39.0 44.0) 10.0
If you do not want to directly processPoint
We can create an auxiliary function (auliliary function), first create a graph from the origin, and then move them.
baseCircle :: Float -> Shape
baseCircle r = Circle (Point 0 0) r
baseRect :: Float -> Float -> Shape
baseRect width height = Rectangle (Point 0 0) (Point width height)
ghci> nudge (baseRect 40 100) 60 23
Rectangle (Point 60.0 23.0) (Point 100.0 123.0)
Without a doubt, you can export your data type to the module. You just need to write your type together with the function to be exported. In the brackets that follow, list the constructors of the values to be exported and separate them with commas. If you want to export all the values to construct the child, write...
To export all functions and types defined here to a module, you can do this:
module Shapes
( Point(..)
, Shape(..)
, surface
, nudge
, baseCircle
, baseRect
) where
OneShape
(...), And we export it.Shape
All values of constructor. This allows anyone to import our modules.Rectangle
AndCircle
Value ConstructorShape
. This is related to writingShape(Rectangle,Circle)
Equivalent.
We can choose not to export anyShape
To construct the sub-function, so that the user using our module can only use the auxiliary function.baseCircle
AndbaseRect
To obtainShape
.Data.Map
This is the set, noMap.Map [(1,2),(3,4)]
Because it does not export any value to the constructor. But you can useMap.fromList
This auxiliary function is used to obtainmap
. It should be remembered that the value constructor is just a function. If you do not export them, you will be denied from using our module to call them. However, you can use other functions that return this type to obtain this type of value.
The value constructor without exporting data types hides their internal implementations, so that the type abstraction is higher. At the same time, the user of our module cannot use this value to construct sub-pattern matching.