Product Types
The reason why we call data constructors data constructors is because they can take arguments. For example, we can define a point in two-dimensional space as
data Point2D = P Double Double
or
data Point2D = Point2D Double Double
In this case, the data constructor is P
(in the first definition) or Point2D
(in the second definition). This demonstrates that the data constructor(s) of a
type can have the same name as the type or a different name. I'll stick to the
first definition for now because different names for the type—Point2D
—and the
data constructor—P
—avoid confusion whether I talk about the type or data
constructor.
To construct a Point2D
, we need two Double
values:
>>> data Point2D = P Double Double
>>> p = P 10.3 20
So Point2D
is really nothing but a pair of Double
s. However, as far as the
compiler is concerned, Point2D
and (Double, Double)
are two different types.
That's exactly the point: By defining a custom type for 2-d points, we make our
intent clear what this pair of Double
s represents—a point—and the compiler can
check that we never pass just any pair of Double
s to a function meant to
manipulate points.
So why do we call things like P
, Red
, Green
, Blue
data constructors? If
you squint at it right, you will see that P
is nothing but a function. It
takes two arguments of type Double
and returns a Point2D
. Indeed, this is
exactly how the Haskell compiler sees it:
>>> :type P
P :: Double -> Double -> Point2D
Naming a function that constructs a Point2D
from its parts a data constructor
is an apt choice. We can similarly view Red
, Green
, and Blue
to be
functions that construct values of type Color
, only they don't take any
arguments, but that's fine.
Our function colorAsString
above showed us that we can use data constructors
as patterns when using pattern matching. The same is true for data constructors
that take arguments, but now we need nested patterns to match the arguments of
the data constructor. Here are two examples that make this clearer:
-
P x y
is a pattern that matches any value of typePoint2D
, because any such value is constructed using the data constructorP
. The inner patterns used to match the twoDouble
values passed toP
are variables here,x
andy
. Since variables match any value, this pattern imposes no constraints on the two double values. Also remember that using a variable as a pattern means that the value matched to the variable as part of pattern matching gets assigned to the variable. So, in this pattern,x
can be used to refer to the first component of the point, its \(x\)-coordinate presumably, andy
can be used to refer to the second component, its \(y\)-coordinate. We can use this to implement functions to access the two coordinates of a point:xCoord, yCoord :: Point2D -> Double xCoord (P x _) = x yCoord (P _ y) = y
or to convert between a point and a plain old pair:
pointAsPair :: Point2D -> (Double, Double) pointAsPair (P x y) = (x, y) pairAsPoint :: (Double, Double) -> Point2D pairAsPoint = uncurry P
Sneaky old me: I'm making sure that you don't forget our good old
uncurry
function used to convert a function that takes two arguments into a function that takes a pair as its only argument. SinceP
has typeDouble -> Double -> Point2D
,uncurry P
has type(Double, Double) -> Point2D
: it converts a pair ofDouble
s into aPoint2D
.There's a better way to define a point type along with accessors to its coordinates. We'll talk about it soon.
-
Just as we can use patterns that match only specific values,
isZero :: Int -> Bool isZero 0 = True isZero _ = False
we can use such values as patterns nested inside a data constructor. For example,
isOnYAxis :: Point2D -> Bool isOnYAxis (P 0 _) = True isOnYAxis _ = False
The first pattern,
P 0 _
matches only points whose \(x\)-coordinate is 0. The wildcard for the \(y\)-coordinate says that we don't care what the \(y\)-coordinate is. Thus, this pattern matches all points with \(x\)-coordinate 0, which are the points on the \(y\)-axis of the coordinate system. If the first pattern does not match, then we know that the \(x\)-coordinate is not 0. In that case, we know that the point is not on the \(y\)-axis, so we don't need to know anything else about the point to answerFalse
: the point is not on the \(y\)-axis. This justifies that we use a wildcard for the whole point in the second equation.Note that in this instance, the pattern
P _ _
would have achieved the same, as this pattern would also match every single point. As we discuss next, this isn't the case for all data types.