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. Lets continue our exploration of Haskell.
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, 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:
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
Heres 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:
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 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%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.