Lecture 4: Secret Code, User-defined datatypes
====================================================
> {-# OPTIONS -Wall -fno-warn-type-defaults #-}
> module Lec4 where
> import Prelude hiding (Maybe,Just,Nothing,Either,Left,Right)
> import Test.HUnit
REMINDER: HW #1 is due tomorrow at 8PM! HW #2 will be available on
Wednesday.
Back to Secret Code
===================
Recall the plan that Chris was working through with you last Wednesday:
1) We will develop a [Haskell program](SecretCode.html) to encode files
from disk.
2) We will [modify the program](SecretCode2.html) so that it can both
encode and decode files.
According to Chris, you worked through the first part of the SecretCode,
but did not finish it. We'll pick up there.
User-defined datatypes
======================
So far, we've mostly talked about how to use the types that appear in
the Haskell standard library. We also discussed a few type synonyms,
like
type XY = (Double,Double)
from the last lecture, but we haven't described any ways to define
really _new_ types.
As a motivating example, suppose you are writing an application that
deals with calendars and you need to represent the days of the week.
You might be tempted to use `String`s or `Int`s, but both of these
choices have issues. If you use
type Day = String
there will be lots of `Day`s that don't actually represent real days.
Also, you will need to devise and adhere to some sort of
standardization - is Monday represented by "Monday", "monday",
"MONDAY", or "Mon"? Should you handle more than one?
The choice
type Day = Int
has similar problems. There are lots of Ints that won't represent
valid days. Also you'll have to remember whether you pick Sunday or
Monday to be the first day of the week, and whether it is represented
by 0 or 1.
Haskell has a better solution: user-defined datatypes:
> data Day = Monday
> | Tuesday
> | Wednesday
> | Thursday
> | Friday
> | Saturday
> | Sunday
> deriving (Show, Eq)
The new values (`Monday`, `Tuesday`, etc.) are called "constructors".
This is a very simple example of a datatype (basically just an
enumeration), but we'll see more examples in a minute.
We can define functions on datatypes by pattern matching! For example:
> nextWeekday :: Day -> Day
> nextWeekday Monday = Tuesday
> nextWeekday Tuesday = Wednesday
> nextWeekday Wednesday = Thursday
> nextWeekday Thursday = Friday
> nextWeekday Friday = Monday
> nextWeekday Saturday = Monday
> nextWeekday Sunday = error "BUG: shouldn't"
This is great. Now we don't have to worry about the difference
between "Monday" and "monday" or which Int corresponds to which day.
If we make a typo (for example, write Frday instead of Friday), the
compiler will warn us _at compile time_. And if we forget to handle
one of the days in some function, the compiler will warn us about that
too (inexhaustive pattern match warning).
Let's write one more function on `Day`s - we can now easily compute
when a package will arrive by "two day" shipping:
> twoBusinessDays :: Day -> Day
> twoBusinessDays = nextWeekday . nextWeekday
Datatypes can hold data, too. For example, here is a datatype for
representing shapes:
> data Shape =
> Circle Float Float Float
> | Rectangle Float Float Float Float
Here, `Circle` and `Rectangle` are the constructors - every `Shape`
must be one or the other. Each constructors has some arguments:
- A `Circle` is specified by three `Floats`. These represent the x
and y coordinates of the center and the radius.
- A `Rectangle` is specifed by four `Floats`. The first two are the
coordinates of the lower left corner, and the second two are the
coordinates of the upper right corner.
We can pattern match on shapes. For example, here is a function that
computes the area of any `Shape`:
> area :: Shape -> Float
> area (Circle _ _ r) = pi * (r * r)
> area (Rectangle x1 y1 x2 y2) = (y2 - y1) * (x2 - x1)
Note that constructors are first-class Haskell values just all the
other values we have seen. Like any value, they have types.
For example the types of `Monday` and `Tuesday` shouldn't surprise you:
Monday :: Day
Tuesday :: Day
The constructors that take arguments have _function_ types. For
example, you must apply `Circle` to three `Float`s to get a `Shape`:
Circle :: Float -> Float -> Float -> Shape
Rectangle :: Float -> Float -> Float -> Float -> Shape
Recursive Types
===============
Datatypes can be defined recursively. That is, their constructors can
take other elements of the same type as arguments.
For example, here is one way to define the type of natural numbers:
> data Nat = Zero
> | Succ Nat
Every natural number is either Zero, or the successor of some other
natural number. For example, we represent 2 as:
> natTwo :: Nat
> natTwo = Succ (Succ Zero)
We could write a function convert `Nat`s to `Int`s. Of course, we'll
do it with pattern matching and recursion:
> natToInt :: Nat -> Int
> natToInt Zero = 0
> natToInt (Succ x) = 1 + natToInt x
We could also define a function to add two natural numbers:
> natPlus :: Nat -> Nat -> Nat
> -- natPlus Zero Zero = Zero
> -- natPlus m Zero = m
> natPlus Zero m = m
> natPlus (Succ x) y = -- natPlus x (Succ y)
> Succ (natPlus x y)
Do they work?
> natToIntTests :: Test
> natToIntTests = TestList [ natToInt Zero ~?= 0,
> natToInt (Succ (Succ (Succ Zero))) ~?= 3 ]
> natPlusTests :: Test
> natPlusTests =
> TestList [ natToInt (natPlus (Succ (Succ Zero)) Zero) ~?= 2,
> natToInt (natPlus (Succ Zero) (Succ (Succ Zero))) ~?= 3]
As another example, we could define a type representing lists of
integers:
> data IntList = INil
> | ICons Int IntList
So that the list 1,2,3 is represented as:
> oneTwoThree :: IntList
> oneTwoThree = ICons 1 (ICons 2 (ICons 3 INil))
For comparison with Haskell's built-in lists, it might help to think
of this as:
> oneTwoThree' :: IntList
> oneTwoThree' = 1 `ICons` (2 `ICons` (3 `ICons` INil))
We can define functions by recursion on these too, of course:
> sumOfIntList :: IntList -> Int
> sumOfIntList = undefined
> main :: IO ()
> main = putStrLn "This is Lec4"
[1] Part of this lecture is taken from "Learn You a Haskell for Great Good".
http://learnyouahaskell.com/