Haskell Project: Show, Compare, and Filter

Now that we have a few basic types we should start working on making their interaction nicer. It is pretty important to be able to print and compare data types so we will start there. Once we are able to compare, we should be able to filter lists of data types.

If you didn't read the previous post catch up on [Stack and Data Types][1]. Next post introduces [Unit Testing][5].

About Data Structures

In the previous post we created a few very basic data types using the type and data keywords. There are three data type keywords and we will quickly cover them here.

Data Type Keyword: type

-- |Name is a string
type Name = String

-- |Age is an Int
type Age = Int
let x = "Joe" :: Name
:t x

x :: Name

x

"Joe"

length x

3

Since we aren't doing anything extremely interesting with `Name`, having it defined as an alias to `String` makes a lot of sense since its mostly for making the code more readable.
let nameLength :: Name -> Int

    nameLength = length

:t nameLength

nameLength :: Name -> Int

nameLength x

3

Since `length` works on `Foldable` class types and `String` is a `Foldable` then our alias works with length as well. (I'll explain type classes later in the post.) Our simple little method to calculate the length of a name is all just syntactic sugar to make the code more readable.

One thing to note though. When creating data types using `type`, type safty is enforced only allowing the casting to work from dirived type to base type. For example, lets say we create two types, `GivenName` and `SirName`:

type GivenName = String

type SirName = String

And we create a method that prints the full name with the following signature:

printName :: GivenName -> SirName -> IO ()

Even though both data types are aliases to `String` you cannot pass a `SirName` for the argument where a `GivenName` is expected.

### Data Type Keyword: data

The next data type keyword we used was `data`. This keyword describes a structure with

- One or more constructors
- Each constructor contains zero or more fields

In the example we will create a `Person` data type that has a single constructor and takes in a `Name` and an `Age`.

-- |Person: Contains a name (string) and an age (int)

data Person = Person Name Age

Another example is to create a data type that represents a department of an business. There are two different types of departments: Empty and Staffed. Both department types have a name but only one has a list of employees.

-- |Department: Two different versions

-- EmptyDepartment: Dept Name

-- StaffedDepartment: Dept Name and list of staff

data Department = EmptyDepartment Name

            | StaffedDepartment Name [Person]

Functions can now interact with the type `Department` but have two different versions to handle.

staffCountInDepartment :: Department -> Int

staffCountInDepartment (EmptyDepartment _) = 0

staffCountInDepartment (StaffedDepartment _ staff) = length staff

Using pattern matching we are able to create two different forms of the `staffCountInDepartment` function which handles an `EmptyDepartment` differently than a `StaffedDepartment`.

Here we also see the difference between the data type and the constructor strings. In the function signature we use `Department` since that is the type. In the implementation we use the constructor names `EmptyDepartment` and `StaffedDepartment`. In our `Person` example since there is only one constructor it is acceptable to have both the data type and constructor to have the same name.

#### Side Note: Pattern Matching and Undscore

Note in the pattern matching you can either fill the field with a name or use `_` to ignore it. Haskell expects all arguments defined in functions and test expressions to be used somewhere in the code. In situations where the value is not needed, using the `_` name tells Haskell that it can ignore the fact that you aren't using the value.

### Data Type Keyword: newtype

The one keyword not used yet is `newtype`. It is similar to `data` and `type`, just with a few odd rules.

- Must create a new constructor (unlike `type`)
- Can only have one constructor (unlike `data`)
- Can only have a single field (like `type`)
- Does not associate functions using similar types (unlike `type`)

From these rules `newtype` really is a *new* type. It contains the data of the type supplied but that is where the association between the new and old types end.
newtype DeptName = DeptName String
let y = DeptName "Accounting"
length y

:29:8:

Couldn't match expected type `t0 a0' with actual type `DeptName'

In the first argument of `length', namely `y'

In the expression: length y

Here you see that even though `DeptName` is based on `String` the function `length` does not operate as expected. We would need to create our own new method to calculate the length.

deptNameLength :: DeptName -> Int

deptNameLength (DeptName n) = length n

Here we are deconstructing the new type, extracting the `String` from it and calling another method. While this seems a bit cumbersome, the benefit here is that it would not be possible to pass in `Name` or any other string type to this function.
let x = DeptName "Accounting"
:t x

x :: DeptName

deptNameLength x

10

let y = "Joe" :: Name
:t y

y :: Name

deptNameLength y

:36:16:

Couldn't match type `[Char]' with `DeptName'

Expected type: DeptName

  Actual type: Name

In the first argument of `deptNameLength', namely `x'

In the expression: deptNameLength x

From the error we see that the function expected `DeptName` but instead was passed `Name`.

One good example of `newtype` is taking a query string and sanitizing it before processing. As a pointless example, lets say our language parser only used capitalized words.

newtype QueryString = QueryString String

newtype SanitizedString = SanitizedString String

sanitize :: QueryString -> SanitizedString

sanitize (QueryString str) = SanitizedString $ map Data.Char.toUpper str

Even though both types look the same, you cannot pass a `SanitizedString` into `sanitize`. If we used `type` this restriction wouldn't be possible.

*Note* There are a few other distinguishing rules about `newtype` I haven't covered. For the moment they aren't too important but hopefully I'll find a way to incorporate them to the application/blog.

## Showing with the Show Type Class

A nice feature of haskell is being able to derive functionality from class types using the `deriving` keyword. Starting with our current implementation of `Date` we derive `Show` and get the following output.
let d = (Date 2000 1 30)
d

Date 2000 1 30

That is nice for basic development and debugging but what if we wanted to change the default way the `Date` type was shown. We could match the format expected by todo.txt, `YYYY-MM-DD`. We just need to overload the functions required by `Show`, namely `show a`.

data Date = Date Integer Int Int

-- |Show Date in YYYY-MM-DD format

instance Show Date where

show (Date year month day) = show year

                           ++ "-" ++ show month

                           ++ "-" ++ show day

Now when we display the data type we get the format we expect.
let d = (Date 2000 1 30)
d

2000-1-30

And with that you've had your first introduction into Type Classes. `Date` is now an instance of the type class `Show`. If you want to see more information about the type class type `:info Show` into your GHCi instance.
:info Show

class Show a where

showsPrec :: Int -> a -> ShowS

show :: a -> String

showList :: [a] -> ShowS

    -- Defined in `GHC.Show'

instance Show a => Show [a] -- Defined in `GHC.Show'

instance Show Word -- Defined in `GHC.Show'

instance Show Ordering -- Defined in `GHC.Show'

instance Show a => Show (Maybe a) -- Defined in `GHC.Show'

instance Show Integer -- Defined in `GHC.Show'

instance Show Int -- Defined in `GHC.Show'

instance Show Char -- Defined in `GHC.Show'

instance Show Bool -- Defined in `GHC.Show'

...

You can see what functions exist in the type class, and instances that exist already.

*Note* For those who noticed, this doesn't exactly fit our format requirement. We'll fix it in the next post.

A little more complicated example would be how to print out a Task. Some of the issues are:

- Incomplete and Completed Tasks look different
- Incomplete Tasks have optional Priority and Start Date

-- |Show Task in format "(A) 2016-07-30 Task to do +Project @Context"

instance Show Task where

show (Completed date task) = "x " ++ (show date) ++ " " ++ (show task)

show (Incomplete mPriority mDate _ _ str) = (showPriority mPriority)

                                          ++ (showDate mDate)

                                          ++ str

where showPriority (Just p) = "(" ++ [p] ++ ") "

      showPriority Nothing = ""

      showDate (Just d) = show d ++ " "

      showDate Nothing = ""

The first two `show` lines use pattern matching to distinguish between `Completed` and `Incomplete` tasks. Since our completed task contains an incomplete task, we are able to invoke show recursively rather than handling the same data twice.

To handle the optional fields we create a couple of helper functions that use pattern matching (there is often a lot of pattern matching in Haskell) to return the appropriate string.

## Comparing with Eq and Ord Type Classes

Our next two type classes deal with comparing values. The `Eq` type class handles equivalency between two values.
:info Eq

class Eq a where

(==) :: a -> a -> Bool

(/=) :: a -> a -> Bool

    -- Defined in `GHC.Classes'

From the info on `Eq` we can see that there are two functions defined: `(==)` and `(/=)`. The first take two instances of the same type and returns `True` if they are equal. The second function is for "not equals".

*Note* the use of parenthesis on function names with symbols makes them prefix notation. `(==) 1 2` is the same as `1 == 2`.

Let implement the equality check for `Date`.

-- |Check if two Dates are equal

instace Eq Date where

(Date y1 m1 d1) == (Date y2 m2 d2) = y1 == y2

                                   && m1 == m2

                                   && d1 == d2

*Note* Due to some magic in the type class, you do not need to implement `(/=)` if you don't want. See the Hackage documentation on [Eq][2] for Minimal complete definition.
(Date 2017 1 1) == (Date 2017 1 1)

True

(Date 2017 1 1) == (Date 1999 12 31)

False

The next type class for comparing is `Ord`, which is used for ordered data types.
:info Ord

class Eq a => Ord a where

compare :: a -> a -> Ordering

(<) :: a -> a -> Bool

(<=) :: a -> a -> Bool

(>) :: a -> a -> Bool

(>=) :: a -> a -> Bool

max :: a -> a -> a

min :: a -> a -> a

The `Ord` type class is defined with a requirement that the type also be an instance of type class `Eq`. The class contains multiple functions but from the Hackage documentation of [Ord][3] we see that the minimal complete definition is to implement `compare`.

-- |Compare (LT,GT,EQ) two Dates

instance Ord Date where

compare (Date y1 m1 d1) (Date y2 m2 d2) = if y1 /= y2

                                        then compare y1 y2

                                        else if m1 /= m2

                                             then compare m1 m2

                                             else compare d1 d2

Our `compare` function is pretty simple to understand. Walking through the Year, Month and Day, if any of them are not equivalent then we use the built in `compare` method.

## Filtering

Now that we can compare our data types, we can filter lists of data types.

The `filter` function has the type definition:

filter :: (a -> Bool) -> [a] -> [a]

The first argument is a function that takes a value and returns a Bool, `True` if the value should be kept in the list. If we wanted to filter a list of `Tasks` and only return the `Incomplete` we could use `filter`.

-- |Filter out all completed tasks and return a list of incomplet

onlyPending :: [Task] -> [Task]

onlyPending = filter isPending

where isPending (Incomplete _ _ _ _ _) = True

    isPending _ = False

A useful function is to check if one list is a subset of another. Using filter we can create such a function.

module Util ( subsetOf ) where

-- |subsetOf filters a list that contains a set of elements
subsetOf :: (Eq a, Foldable t) => [a] -> t a -> Bool
xs `subsetOf` ys = null $ filter (not . (`elem` ys)) xs

To break this down we will start inside out.

(not . (`elem` ys))

Check if the argument is an element of ys, and invert the result.

filter (not . (`elem` ys)) xs

Running over xs each element is kept if it is not an element of ys.

null $ filter (not . (`elem` ys)) xs

Returns True if the list passed to null is empty.

If there are any values in xs that are not in ys they will be returned by filter and since the list passed to null would not be empty it returns False.

Using subsetOf we can filter for tasks that contain all the projects in a list.

-- |Filter based on Projects. Strings for project should not contain '+' as
-- defined by todo.txt
filterProjects :: [Project] -> [Task] -> [Task]
filterProjects px = filter projectFilter
  where projectFilter (Incomplete _ _ projs _ _) = px `subsetOf` projs
        projectFilter (Completed _ t) = projectFilter t

Up next

If you want to see all the modifications for this post you can diff the code [here][4].

In the next post we will fix some of the bugs we introduced in this post by writing some Unit Tests.

=> Haskell Project: Stack and Data Types [1] | Prelude - Eq [2] | Prelude - Ord [3] | Code [4] | Haskell Project: Unit Testing with Hspec [5]

$ date: 2017-05-09 19:02 $

$ tags: haskell, tutorial $

-- CC-BY-4.0 jecxjo 2017-05-09

=> Comments? | back

Proxy Information
Original URL
gemini://gemini.sh0.xyz/log/2017-05-09-haskell-project-show-compare-and-filter.gmi
Status Code
Success (20)
Meta
text/gemini
Capsule Response Time
525.720867 milliseconds
Gemini-to-HTML Time
6.057823 milliseconds

This content has been proxied by September (ba2dc).