Skip to content

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 Points 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 Points 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.