[Published in Open Source For You (OSFY) magazine, November 2014 edition.]
In this article, we shall explore more type classes in Haskell. Consider the Functor type class:
class Functor f where
fmap :: (a -> b) -> f a -> f b
It defines a function fmap, that 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 types ‘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. Firstly, it must satisfy the identity property where running the map over an id must return the id:
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 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:
- Identity value
- 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:
class Monad m where
(>>=) :: 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:
instance Monad Maybe where
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:
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 organize 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:
module Control.Monad.Writer.Class (
MonadWriter(..),
listens,
censor,
) where
...
You can import a module in your code or at the GHCi prompt, using the following command:
import Control.Monad.Writer
If you want to use only selected functions, you can selectively import them using:
import Control.Monad.Writer(listens)
If you want to import everything except a particular function, you can hide it while importing, as follows:
import Control.Monad.Writer hiding (censor)
If two modules have the same function names, you can explicitly use the fully qualified name, as shown below:
import qualified Control.Monad.Writer
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:
import qualified Control.Monad.Writer as W
You can then invoke the ‘listens’ function using W.listens.
Let us take 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 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%Q@ == @%%Y-%m-%dT%%H:%M:%S%Q@.
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.
formatPadded :: UTCTime -> String
formatPadded t
| 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.