Unions of Product Types
So far, we have discussed that we can define simple enumeration types using multiple data constructors that don't take arguments:
data Color = Red | Green | Blue
and simple product or tuple types that have a single data constructor with arguments:
data Point2D = P Double Double
It shouldn't be surprising that we can combine these two to define types with multiple data constructors that do take arguments. For example,
data Point
= P2 Double Double
| P3 Double Double Double
This point type has two data constructors P2
and P3
that take two or three
arguments, presumably to represent points in two or three dimensions. While the
pattern P x y
matched any Point2D
in the previous subsection, because there
was only one data constructor P
for the Point2D
type, the pattern P2 x y
now matches only Point
s with two coordinates, and P3 x y z
matches only
points with three coordinates. For example,
is3d :: Point -> Bool
is3d (P3 _ _ _) = True
is3d _ = False
If the point was constructed using P3
, it has three coordinates and thus is a
3-d point. We use wildcards for the coordinates because we don't need to know
the coordinates in this case. If the first equation does not match, the point
must have been constructed using the P2
data constructor, so it's not a 3-d
point; it has only two coordinates. We don't need to explicitly match against
the pattern P2 _ _
here, because we already know that the point will match
this pattern, given that it doesn't match the pattern P3 _ _ _
if we reach the
second equation.
Let's revisit our isOnYAxis
predicate, only we want it to work for Point
s now:
isOnYAxis :: Point -> Bool
isOnYAxis (P2 0 _ ) = True
isOnYAxis (P3 0 _ 0) = True
isOnYAxis _ = False
This says that a 2-d point P2 x y
is on the \(y\)-axis if x == 0
, the first
pattern. A 3-d point is on the \(y\)-axis if x == 0
and z == 0
, the second
pattern. Any point that does not match either of these two patterns is not on
the \(y\)-axis. So, if we reach the third equation, we don't care what the point
looks like to know that it's not on the \(y\)-axis. We use the wildcard in the
third equation to not even inspect whether the point is a P2
or P3
.
In the Point2D
version of isOnYAxis
, the patterns _
and P _ _
were
semantically equivalent because any point matches either pattern. If we want to
use isOnYAxis
using data constructors throughout, the definition becomes more
complex,
isOnYAxis :: Point -> Bool
isOnYAxis (P2 0 _ ) = True
isOnYAxis (P3 0 _ 0) = True
isOnYAxis (P2 _ _ ) = False
isOnYAxis (P3 _ _ _) = False
because the pattern P2 _ _
only matches arbitrary 2-d points, and the pattern
P3 _ _ _
only matches arbitrary 3-d points. This definition is arguably worse
than the previous one because it isn't only longer, it also pretends that it is
important to distinguish between 2-d and 3-d points in equations 3 and 4.
Clearly, this distinction is unimportant, so our code becomes more readable (and
more efficient as a by-product) if we replace the last two equations with the
single equation isOnYAxis _ = False
.