Nesting Custom Types

Learn to define nested Haskell data types.

Defining nested types

When defining custom types with data, we are not limited to using predefined types in the constructors. We can also nest custom types.

Let’s extend our Geometry type (from the previous lesson) with location information. Remember, its definition was

data Geometry = Rectangle Double Double | Square Double | Circle Double deriving (Show)

The location of a shape should be the coordinates of its center point in the 2D plane. We thus identify a location by two doubles, its x and y coordinates.

data Coordinates = Coordinates Double Double deriving (Show)

Note that we are using Coordinates here as both the name of the type as well as the name of the only constructor. This is fine, as types and constructors have different namespaces. By convention, types with a single constructor often have the same name as the constructor.

We can now create a new type for a shape with the coordinates of its center.

data LocatedShape = LocatedShape Geometry Coordinates deriving (Show)

Located shapes can be created by nesting constructors.

located = LocatedShape (Square 2) (Coordinates 2 3)

Functions on nested types

Given a located shape, let’s write a function that takes another coordinate point and checks whether it is contained inside the shape.

containedIn :: Coordinates -> LocatedShape -> Bool

The function is used like this:

*Geometry> (Coordinates 5 3) `containedIn` (LocatedShape (Square 2) (Coordinates 3 3))
False

Here is our definition of containedIn.

containedIn :: Coordinates -> LocatedShape -> Bool
(Coordinates x y) `containedIn` (LocatedShape shape (Coordinates cx cy)) = 
  case shape of
    (Circle r) -> (cx - x)^2 + (cy - y)^2 <= r^2
    (Rectangle a b) -> abs (cx - x) <= a / 2 && abs (cy - y) <= b / 2
    (Square a) -> abs (cx - x) + abs (cy - y) <= a

At the beginning of the function equation, we are applying nested pattern matching on the located shape.

(LocatedShape shape (Coordinates cx cy))

We already deconstructed the nested Coordinates value and bound the coordinates to cx and cy. On the other hand, the shape variable will bind to the Geometry value in the located shape, which we don’t yet fully match. We defer matching on the shape to define the function in one equation. This makes it more readable compared to the alternative of matching everything.

-- full pattern matching version
containedIn :: Coordinates -> LocatedShape -> Bool
(Coordinates x y) `containedIn` (LocatedShape (Circle r) (Coordinates cx cy)) = (cx - x)^2 + (cy - y)^2 <= r^2
(Coordinates x y) `containedIn` (LocatedShape (Rectangle a b) (Coordinates cx cy)) = abs (cx - x) <= a / 2 && abs (cy - y) <= b / 2
(Coordinates x y) `containedIn` (LocatedShape (Square a) (Coordinates cx cy)) = abs (cx - x) + abs (cy - y) <= a

The full pattern matching version contains a lot of duplicated code because the pattern matches in the three equations, which is why the delayed matching on the shape is preferable. When complex pattern matches are involved, it is cleaner to match the types that do not have several alternative constructors (like Coordinates ) once in the beginning. Then, use case to continue matching on types that do have several alternative constructors (like Geometry).

Exercise: Pattern matching on custom types

As an exercise implement a function move which takes a LocatedShape and moves it to a new location. The direction of movement is given by a movement vector, which should be added to the coordinates of the shape.

data Vector = Vector Double Double

For example, if the shape is at Coordinates 2 3 and the movement vector, Vector 5 1, is passed, the shape should move to Coordinates 7 4. The Geometry value of the shape should remain unchanged.

move (LocatedShape (Circle 5) (Coordinates 2 3)) (Vector 5 1) = LocatedShape (Circle 5) (Coordinates 7 4)

Get hands-on with 1400+ tech skills courses.