io7m | single-page | multi-page | archive (zip, signature)
3. What?A Crash Course In Algebraic Types5. The Type Zoo
PreviousUpNext

Shapes
Declarations
In keeping with traditional introductory texts on programming languages, the following definitions declare a collection of "shape" types:
module Shapes where

data Circle =
  MakeCircle Integer
  deriving Show

data Rectangle =
  MakeRectangle Integer Integer
  deriving Show

data Shape
  = ShapeCircle Circle
  | ShapeRectangle Rectangle
  deriving Show
Each data declaration declares an algebraic data type. The Circle declaration, for example, declares a type Circle with a single data constructor named MakeCircle, which takes a single value of type int as an argument. That is, the programmer calls MakeCircle to create a value of type Circle. It's important to note that the MakeCircle function is the only way to create a value of type Circle - this simple fact alone has far reaching consequences and can be used to express and enforce various program invariants (a point that this document will address later). The definitions also ask Haskell, via the deriving statement, to provide default implementations of the string conversion functions, for the sake of being able to display values in the interpreter for examples in this document - these can be ignored.
The Rectangle declaration is similar, except that its single data constructor MakeRectangle takes two integers as input (representing the width and height of the rectangle).
The last declaration, Shape, declares two data constructors: ShapeCircle and ShapeRectangle. That is, there are two ways to obtain a value of type Shape: either call ShapeCircle with a value of type Circle, or call ShapeRectangle with a value of type Rectangle.
It's possible to see how these declarations work by constructing values in the interpreter and checking their types (the :: notation can be read as "is of type" - "23 :: Integer" means "23 is of type Integer"). The :type command, unsurprisingly, displays the type of the given expression.
*Shapes> :type MakeCircle 23
MakeCircle 23 :: Circle

*Shapes> :type MakeRectangle 23 11
MakeRectangle 23 11 :: Rectangle

*Shapes> :type ShapeCircle (MakeCircle 23)
ShapeCircle (MakeCircle 23) :: Shape

*Shapes> :type ShapeRectangle (MakeRectangle 23 11)
ShapeRectangle (MakeRectangle 23 11) :: Shape
Note that it's a type error to attempt to pass a Rectangle to the ShapeCircle constructor:
*Shapes> :type ShapeCircle (MakeRectangle 23 11)
<interactive>:1:14:
    Couldn't match expected type `Circle' with actual type `Rectangle'
    In the return type of a call of `MakeRectangle'
    In the first argument of `ShapeCircle', namely
      `(MakeRectangle 23 11)'
    In the expression: ShapeCircle (MakeRectangle 23 11)

*Shapes> :type ShapeRectangle (MakeCircle 23)
<interactive>:1:17:
    Couldn't match expected type `Rectangle' with actual type `Circle'
    In the return type of a call of `MakeCircle'
    In the first argument of `ShapeRectangle', namely `(MakeCircle 23)'
    In the expression: ShapeRectangle (MakeCircle 23)
Matching
Much as values are created via constructors, it's necessary to deconstruct data structures in order to get at the values contained within. This deconstruction is performed via pattern matching. Most languages allow one to, for example, pass an enumerated type into some form of case statement and then perform some action for each one of the individual cases. Pattern matching, however, allows the programmer to perform actions based on the actual structure of the value passed in. As an example, a simple function that takes any value of type Shape and prints the name of the shape [1]:
module ShapeShow where

import Shapes

shape_show :: Shape -> IO ()
shape_show s =
  case s of
    ShapeRectangle _ -> print "rectangle"
    ShapeCircle _    -> print "circle"
Unsurprisingly, the case expression attempts to match the input value against each pattern on the left hand side in turn, from top to bottom, stopping at the first pattern that matches (there is no "fall-through" as with switch statements in other languages). This naturally leads to patterns being arranged in decreasing order of specificity (most specific patterns first).
Notice that by pattern matching on a value of type Shape, the programmer learns which constructor was used to construct the value and can perform an action for each. The programmer is also statically prevented from failing to handle a case, to ensure correctness - the compiler warns against patterns that are redundant (unreachable) or that aren't exhaustive [2]:
module ShapeShowNE where

import Shapes

shape_show_ne :: Shape -> IO ()
shape_show_ne s =
  case s of
    ShapeRectangle _ -> print "rectangle"
ShapeShowNE.hs:7:3:
  Warning: Pattern match(es) are non-exhaustive
    In a case alternative: Patterns not matched: ShapeCircle _
module ShapeShowOL where

import Shapes

shape_show_ol :: Shape -> IO ()
shape_show_ol s =
  case s of
    ShapeRectangle _ -> print "rectangle"
    ShapeRectangle _ -> print "rectangle"
ShapeShowOL.hs:7:3:
  Warning: Pattern match(es) are overlapped
    In a case alternative: ShapeRectangle _ -> ...
The warning about non-exhaustive cases is extremely important with regards to software maintenance: Add another constructor to a data type and the compiler will immediately inform the programmer of every point in the code that now needs to be modified to work correctly.
A similar function that prints the width of any shape:
module ShapeWidth where

import Shapes

shape_width :: Shape -> IO ()
shape_width s =
  case s of
    ShapeRectangle (MakeRectangle width _) -> print width
    ShapeCircle (MakeCircle radius)        -> print (2 * radius)
The names used on the left hand side of the case expression are given by the programmer for use in the expressions on the right hand side. That is, the programmer chooses the names that he/she wishes to give to each field he/she is interested in, at the point of use. The _ or wildcard symbol is used to indicate that the programmer doesn't care about a particular value and therefore doesn't want to provide a name. Wildcards match everything. A silly function that prints "Boo!" regardless of whatever's passed in:
module ShapeBoo where

import Shapes

shape_boo :: Shape -> IO ()
shape_boo s =
  case s of
    _ -> print "Boo!"
Patterns consist of constants [3], variables, and wildcards. An example of a function that compares shapes for equality with slightly more complex patterns:
module ShapeEquals where

import Shapes

shape_equals :: Shape -> Shape -> Bool
shape_equals s t =
  case (s, t) of
    (ShapeCircle    (MakeCircle r0),       ShapeCircle    (MakeCircle r1))       -> r0 == r1
    (ShapeRectangle (MakeRectangle w0 h0), ShapeRectangle (MakeRectangle w1 h1)) -> (w0 == w1) && (h0 == h1)
    (_, _)                                                                       -> False
The Haskell definition of the function written with pattern matching is almost exactly what one would expect given the typical informal mathematical specification of the function:
  1. If both shapes are circles, then both shapes are equal iff their radii are equal.
  2. If both shapes are rectangles, then both shapes are equal iff both their widths and heights are equal.
  3. Otherwise, the shapes are not equal.

[1]
The IO type in the signature of the function indicates that the function performs I/O. See Input/Output in Haskell 98.
[2]
Unfortunately, the ghc compiler has all warnings disabled by default, so it's necessary to use -fwarn-incomplete-patterns at the command line to see warnings. The author recommends running with -W -Wall -Werror at all times!
[3]
Constructors are considered to be constants when matching.

PreviousUpNext
3. What?A Crash Course In Algebraic Types5. The Type Zoo