Declarations
In keeping with traditional introductory texts on programming languages, the
following definitions declare a collection of "shape" types:
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.
Note that it's a type error to attempt to pass a Rectangle
to the ShapeCircle constructor:
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 :
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 :
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:
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:
Patterns consist of constants , variables,
and wildcards. An example of a function that compares shapes for equality with
slightly more complex patterns:
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: