Nesting Custom Types
Learn to define nested Haskell data types.
We'll cover the following
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.