Exploring a Few Type Classes in Haskell

2.15K

Haskell, an open source programming language, is the outcome of 20 years of research. It is named after the logician, Haskell Curry. It has all the advantages of functional programming and an intuitive syntax based on mathematical notation. Lets continue our exploration of Haskell.

class Functor f where
fmap :: (a -> b) -> f a -> f b

It defines a function fmap, which accepts a function as an argument that takes input of Type a and returns Type b, and applies the function on every Type a to produce Type b. The f is a type constructor. An array is an instance of the Functor class and is defined as shown below:

instance Functor [] where
fmap = map

The Functor type class is used for types that can be mapped over. Examples of using the Functor type class for arrays are shown below:

ghci> fmap length [abc, defg]
[3,4]

ghci> :t length
length :: [a] -> Int

ghci> map length [abc, defg]
[3,4]

ghci> :t map
map :: (a -> b) -> [a] -> [b]

An instance of a Functor class must satisfy two laws. First, it must satisfy the identity property where running the map over an id must return the id. The term id represents identity.

fmap id = id

For example:

ghci> id [abc]
[abc]

ghci> fmap id [abc]
[abc]

Second, if we compose two functions and fmap over it, then it must be the same as mapping the first function with the Functor, and then applying the second function as shown below:

fmap (f . g) = fmap f . fmap g

This can also be written for a Functor F as follows:

fmap (f . g) F = fmap f (fmap g F)

For example:

ghci> fmap (negate . abs) [1, 2, 3, 4, 5]
[-1,-2,-3,-4,-5]

ghci> fmap negate (fmap abs [1, 2, 3, 4, 5])
[-1,-2,-3,-4,-5]

The Maybe data type can also be an instance of the Functor class:

data Maybe a = Just a | Nothing
deriving (Eq, Ord)

instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing

For example:

ghci> fmap (+2) (Nothing)
Nothing

ghci> fmap (+2) (Just 3)
Just 5

The two laws hold good for the Maybe data type:

ghci> id Nothing
Nothing

ghci> id Just 4
Just 4

ghci> fmap (negate . abs) (Just 4)
Just (-4)

ghci> fmap negate (fmap abs (Just 4))
Just (-4)

The Applicative type class is defined to handle cases where a function is enclosed in a Functor, like Just (*2):

class Functor f => Applicative f where
-- | Lift a value.
pure :: a -> f a

-- | Sequential application.
(<*>) :: f (a -> b) -> f a -> f b

The <\$> is defined as a synonym for fmap:

(<\$>) :: Functor f => (a -> b) -> f a -> f b
f <\$> a = fmap f a

The Applicative Functor must also satisfy a few mathematical laws. The Maybe data type can be an instance of the Applicative class:

instance Applicative Maybe where
pure = Just
(Just f) <*> (Just x) = Just (f x)
_ <*> _ = Nothing

A few examples of Maybe for the Applicative type class are shown below:

ghci> import Control.Applicative

ghci> Just (+2) <*> Just 7
Just 9

ghci> (*) <\$> Just 3 <*> Just 4
Just 12

ghci> min <\$> Just 4 <*> Just 6
Just 4

ghci> max <\$> Just Hello <*> Nothing
Nothing

ghci> max <\$> Just Hello <*> Just World
Just World

The Applicative Functor unwraps the values before performing an operation.
For a data type to be an instance of the Monoid type class, it must satisfy two properties:
1. Identity value
2. Associative binary operator
a * (b * c) = (a * b) * c
These are defined in the Monoid type class:

class Monoid a where
mempty :: a -- identity
mappend :: a -> a -> a -- associative binary operation

Lists can be a Monoid. The identity operator is [] and the associative binary operator is (++). The instance definition of lists for a Monoid is given below:

instance Monoid [a] where
mempty = []
mappend = (++)

Some examples of lists as Monoid are shown below:

ghci> import Data.Monoid

ghci> (a `mappend` b) `mappend` c
abc

ghci> a `mappend` (b `mappend` c)
abc

ghci> mempty `mappend` [5]
[5]

The Monad type class takes a wrapped value and a function that does some computation after unwrapping the value, and returns a wrapped result. The Monad is a container type and hence a value is wrapped in it. The bind operation (>>=) is the important function in the Monad class that performs this operation. The return function converts the result into a wrapped value. Monads are used for impure code where there can be side effects, for example, during a system call, performing IO, etc. A data type that implements the Monad class must obey the Monad Laws. The definition of the Monad class is as follows:

(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a

The Maybe type is an instance of a Monad and is defined as:

return x = Just x
Nothing >>= f = Nothing
Just x >>= f = f x
fail _ = Nothing

So, when m is Maybe, and a and b are of Type Int, the bind operation becomes:

(>>=) :: Maybe Int -> (Int -> Maybe Int) -> Maybe Int

Heres an example of how the Maybe Monad is used:

ghci> return (Just 5)
Just 5

ghci> return Nothing
Nothing

ghci> Just 5 >>= \x -> return (x + 7)
Just 12

ghci> Nothing >>= \x -> return (x + 7)
Nothing

ghci> Just 5 >>= \x -> return (x + 7) >>= \y -> return (y + 2)
Just 14

The newtype keyword is used in Haskell to define a new data type that has only one constructor and only one field inside it. The Writer data type can be defined using the record syntax as follows:

newtype Writer w a = Writer { runWriter :: (a, w) }

It can be an instance of a Monad as follows:

import Data.Monoid

newtype Writer w a = Writer { runWriter :: (a, w) }

instance (Monoid w) => Monad (Writer w) where
return x = Writer (x, mempty)
(Writer (x,v)) >>= f = let (Writer (y, v)) = f x in Writer (y, v `mappend` v)

To test the definition, you can write a double function as shown below:

double :: Int -> Writer String Int
double x = Writer (x * 2,  doubled  ++ (show x))

You can execute it using:

ghci> runWriter \$ double 3
(6, doubled 3)

ghci> runWriter \$ double 3 >>= double
(12, doubled 3 doubled 6)

The evaluation for the bind operation is illustrated below:

ghci> runWriter \$ double 3 >>= double
(12, doubled 3 doubled 6)

ghci> runWriter \$ ((double 3) >>= double)
(12, doubled 3 doubled 6)

ghci> runWriter \$ ((Writer (6, doubled 3)) >>= double)
(12, doubled 3 doubled 6)

The arguments to runWriter are matched to the bind function definition in the Writer Monad. Thus, x == 6, v == doubled 3, and f == double. The function application of f x is double 6 which yields (12, doubled 6). Thus y is 12 and v is doubled 6. The result is wrapped into a Writer Monad with y as 12, and the string v concatenated with v to give doubled 3 doubled 6. This example is useful as a logger, where you want a result and log messages appended together. As you can see, the output differs with input, and hence this is impure code that has side effects.
When you have data types, classes and instance definitions, you can organise them into a module that others can reuse. To enclose the definitions inside a module, prepend them with the module keyword. The module name must begin with a capital letter followed by a list of types and functions that are exported by the module. For example:

listens,
censor,
) where

...

You can import a module in your code or at the GHCi prompt, using the following command:

If you want to use only selected functions, you can selectively import them using:

If you want to import everything except a particular function, you can hide it while importing, as follows:

If two modules have the same function names, you can explicitly use the fully qualified name, as shown below:

You can then explicitly use the listens functions in the module using Control.Monad.Writer.listens. You can also create an alias using the as keyword:

You can then invoke the listens function using W.listens.
Let us look at an example of the iso8601-time 0.1.2 Haskell package. The module definition is given below:

module Data.Time.ISO8601
( formatISO8601
, formatISO8601Millis
, formatISO8601Micros
, formatISO8601Nanos
, formatISO8601Picos
, formatISO8601Javascript
, parseISO8601
) where

It then imports a few other modules:

import Data.Time.Clock (UTCTime)
import Data.Time.Format (formatTime, parseTime)
import System.Locale (defaultTimeLocale)
import Control.Applicative ((<|>))

This is followed by the definition of functions. Some of them are shown below:

-- | Formats a time in ISO 8601, with up to 12 second decimals.
--
-- This is the `formatTime` format @%FT%T%[email protected] == @%%Y-%m-%dT%%H:%M:%S%[email protected]
formatISO8601 :: UTCTime -> String
formatISO8601 t = formatTime defaultTimeLocale %FT%T%QZ t

-- | Pads an ISO 8601 date with trailing zeros, but lacking the trailing Z.
--
-- This is needed because `formatTime` with %Q does not create trailing zeros.
| length str == 19 = str ++ .000000000000
| otherwise = str ++ 000000000000
where
str = formatTime defaultTimeLocale %FT%T%Q t

-- | Formats a time in ISO 8601 with up to millisecond precision and trailing zeros.
-- The format is precisely:
-- >YYYY-MM-DDTHH:mm:ss.sssZ
formatISO8601Millis :: UTCTime -> String
formatISO8601Millis t = take 23 (formatPadded t) ++ Z
...

The availability of free and open source software allows you to learn a lot from reading the source code, and it is a very essential practice if you want to improve your programming skills.

Tags : ,
Author
The author is a free software developer at the Fedora project, and also a blogger. He co-maintains the Fedora Electronic Lab project.

,

,

,

,