Skip to content

mtanzim/haskellbook

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

334 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

haskellbook

Exercises and content for the Haskell programming from first principles

Why?

  • Curiosity around functional programming concepts and purely functional programming

Related efforts

Related resources

Repositories that helped along the way

Notes on critical concepts

Table of contents

Type classes

  • Back to TOC

  • Chapter exercises

  • In Haskell, a declaration of a type defines how that type is constructed

  • A declaration of a type class on the other hand defines how a set of types are consumed

  • Type classes allow for generalizations over a set of types in order to define and execute a set of features for those types

  • One example, it is very useful to be able to test values for equality. So, any type that implements the Eq type class, it's values can be tested for equality

  • As long as a datatype implements or instantiates the Eq type class, the functions == and /= can be used

  • Looking at an example datatype, Bool instantiates the following type classes:

Prelude> :info Bool
data Bool = False | True
instance Eq Bool
instance Ord Bool
instance Show Bool
instance Read Bool
instance Enum Bool
instance Bounded Bool
  • Looking at an example type type class:
Prelude> :info Eq
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
  • The above states, for any type a to implement the Eq type class, it must define the == and \= functions that follow the required signature

  • Attempting to write a type class instance of a new data type, we arrive at:

data Trivial = Trivial'
instance Eq Trivial where
  Trivial' == Trivial' = True
  • Note that \= can be derived from == and thus its definition is not required for the minimal implementation

  • Looking at another example:

data Identity a = Identity a
instance Eq (Identity a) where
  (==) (Identity v) (Identity v') = v == v'
  • Type classes are automatically dispatched by type

Algebraic datatypes

  • Back to TOC

  • Chapter exercises

  • A type can be thought of as an enumeration of constructors that take zero or more arguments

  • Haskell offers the following: sum types, product types (including record syntax), type aliases, and a special datatype called newtype (which will not be covered in the notes)

  • Haskell has 2 types of constructors: type constructors and data constructors

  • Type constructors are used only at the type level: in type signatures, and type class declarations/instances

  • Data constructors create values at term level (values that can be interacted with at run time)

  • Type and data constructors not taking any arguments are constants, ie: Bool is a type constant, and True and False are data constants

  • However, we need to allow different types or amounts of data to be store in our data types. In these times, the type and data constructors are parametrized

  • In these cases, the constructors are like functions, as they must be applied to become a concrete type of value

data Trivial = Trivial'
data UnaryTypeCon a = UnaryValueCon a
  • In the above, Trivial is a constant value at term level; it takes no arguments and this is called nullary or a type constant
  • Trivial' is a constant value as it exists at term level, or run time space
  • UnaryTypeCon is a type constructor with one argument, awaiting a type constant to be applied to it
  • UnaryValueCon is a data constructor awaiting a value to be applied to it
  • The same is true of list datatype
  • At the type level, we have a : [a], where a is a variable
  • At runtime, when a type of value can be applied, it can become concrete, ie: [Char] or [Int]
  • This leads us to the idea of kinds, these are the types of the type constructors
  • Kinds are not types until they are fully applied
  • The fully applied kind is denoted as *, where is the kind * -> * is awaiting a single * to be applied
  • Anything that is awaiting application is a higher kinded datatype, an example is lists as seen above
  • Let's see some examples
-- identical to (a, b, c, d)
data Silly a b c d =
  MkSilly a b c d deriving Show
Prelude> :kind Silly
Silly :: * -> * -> * -> * -> *

Prelude> :kind Silly Int
Silly Int :: * -> * -> * -> *

Prelude> :kind Silly Int String Bool String
Silly Int String Bool String :: *
  • The number of arguments a type or data constructor takes is its arity, ie: nullary, unary

  • The number of possible values a datatype can hold is its cardinality

  • For example, Bool has a cardinality of 2 (True | False), whereas Int8 has a cardinality of 256 (-128 to 127)

Sum types

  • An example : data Bool = False | True
  • | represents a logical disjunction aka or, this is the sum in algebraic datatypes
  • To know the cardinality of sum types, we add the cardinalities of it data constructors; so for Bool, it is 2, True and False

Product types

  • A product type’s cardinality is the product of the cardinalities of its inhabitants
  • A product type expresses and
  • Tuples are an example of a product type: (,) :: a -> b -> (a, b)
  • It allows us to have two kinds of data, each with their own type (or not)
  • Following is an example of a product type with the record syntax
data Person = Person { name :: String, age :: Int }
  • You may combine sum and product types, for example
data Gender = Male | Female
data Person = Person { name :: String, age :: Int, gender: Gender }
  • The function type (a -> b) has exponential cardinality, ie: (Bool -> Bool) has the cardinality of 2 ^ 2

Monoid and Semigroup

mappend [1, 2, 3] [4, 5, 6]
-- [1,2,3,4,5,6]
mappend [1..5] []
-- [1..5]
mappend [] [1..5]
-- [1..5]
-- Or more generally
mappend x mempty == x
mappend mempty x == x
  • In Haskell, we define a type class for monoid as follows:
class Semigroup m => Monoid m where
  mempty :: m
  mappend :: m -> m -> m
  mconcat :: [m] -> m
  mconcat = foldr mappend mempty
  • Taking a concrete example for lists in Haskell:
instance Semigroup [a] where
  (<>) = (++)
instance Monoid [a] where
  mempty = []
  • Monoid laws can be highlighted as follows:
-- left identity
mappend mempty x == x
-- right identity
mappend x mempty == x
-- associativity
mappend x (mappend y z) == mappend (mappend x y) z
mconcat == foldr mappend mempty
  • A Semigroup is only a binary associative operation without an indentity.
class Semigroup a where
(<>) :: a -> a -> a

Functor

  • Back to TOC

  • Chapter exercises

  • A functor is a way of a applying a function inside of a structure, without altering the structure

  • Seeing this with a type class:

class Functor f where
  fmap :: (a -> b) -> f a -> f b
  • Taking a concrete example with lists
Prelude> map (\x -> x > 3) [1..6]
[False,False,False,True,True,True]
  • Note that for a type to have a Functor instance, it must be of kind * -> * or higher
  • For example, the following will NOT work:
data FixMePls = FixMe | Pls deriving (Eq, Show)
instance Functor FixMePls where
  fmap = error "it doesn't matter, it won't compile"
  • The reason is that FixMePls is of kind *
  • Lists however, are of kind * -> *, because [] -> a -> [a] or in English: you need to provide the type of the member of the list to get the list type (ie: Int)
  • With that in mind, the following will work:
data FixMePls a = FixMe | Pls a deriving (Eq, Show)
instance Functor (FixMePls a) where
  fmap _ FixMe = FixMe
  fmap f (Pls a) = Pls (f a)
  • You can stack functors, ie:
GHCi, version 8.10.7: https://www.haskell.org/ghc/  :? for help
Prelude> n = Nothing
Prelude> w = Just "woo"
Prelude> ave = Just "ave"
Prelude> lms = [n,w,ave]
Prelude> replaceWithP = const 'p'
Prelude> fmap replaceWithP lms
"ppp"
Prelude> (fmap . fmap) replaceWithP lms
[Nothing,Just 'p',Just 'p']
Prelude> (fmap . fmap . fmap) replaceWithP lms
[Nothing,Just "ppp",Just "ppp"]

Functor laws

  • Identity
fmap id == id
  • Composition
fmap (f . g) = fmap f . fmap g
  • Preserve structure
fmap :: Functor f => (a -> b) -> f a -> f b

Applicative

  • Back to TOC

  • Chapter exercises

  • Applicatives are monoidal functors

  • With functors we apply a function inside a structure, with applicatives, the function itself is inside of a structure

  • The monoid part is responsible to merging the structures together

  • For example, let's see a list of functions apped to a list of integers

GHCi, version 8.10.7: https://www.haskell.org/ghc/  :? for help
Prelude> fns = [(+1), (*2)]
Prelude> nums = [1,2,3]
Prelude> fns <*> nums
[2,3,4,2,4,6]
Prelude>
  • Note how the results of (+1) being [2,3,4] are concatenated (monoidal operation mappend) with the results of (*2) being [2,4,6]

  • Defining applicative as a type class:

class Functor f => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
  • Note how every type that can have an applicative instance must also have a functor instance
  • The pure function lifts something into the functorial/applicative structure
  • <*> is often called ap or apply
  • Noting the similarities between <*> and fmap or <$>:
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
  • The above types demonstrates:

    with applicatives, the function itself is inside of a structure

Where are the monoids?

  • We claimed applicatives are monoidal functors
  • Firstly, note the types of the following
($) :: (a -> b) -> a -> b
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
  • Looking closer at the <*>, we we notice there are 2 arguments: f (a -> b) and f a, or put in another way:
:: f (a -> b) -> f a -> f b
f           f     f
(a -> b)    a     b
  • Now looking at the definition of mappend: mappend :: Monoid a => a -> a -> a
  • We can see that we have Monoid for our structure (f) and function application for the values
mappend   :: f                f           f
$         :: (a -> b)         a           b
(<*>)     :: f (a -> b) ->    f a ->      f b
  • We can see the monoid aspect of applicatives in action with Haskell tuples
Prelude> fmap (+1) ("blah", 0)
("blah",1)
  • Whereas with applicatives:
Prelude> ("Woo", (+1)) <*> (" Hoo!", 0)
("Woo Hoo!", 1)

Prelude> (Sum 2, (+1)) <*> (Sum 0, 0)
(Sum {getSum = 2},1)

Prelude> (Product 3, (+9))<*>(Product 2, 8)
(Product {getProduct = 6},17)
  • This is seen in the instance definition themselves, ie for tuples:
instance (Monoid a, Monoid b) => Monoid (a,b) where
  mempty = (mempty, mempty)
  (a, b) `mappend` (a',b') = (a `mappend` a', b `mappend` b')

instance Monoid a => Applicative ((,) a) where
  pure x = (mempty, x)
  (u, f) <*> (v, x) = (u `mappend` v, f x)

Usages of applicatives

[(+1), (*2)] <*> [2, 4]
[ (+1) 2 , (+1) 4 , (*2) 2 , (*2) 4 ]
Prelude> (,) <$> [1, 2] <*> [3, 4]
[(1,3),(1,4),(2,3),(2,4)]
  • The above can be thought of as:
Prelude> (,) <$> [1, 2] <*> [3, 4]

fmap the (,) over the first list:
[(1, ), (2, )] <*> [3, 4]

Then, we apply the first list to the second:

[(1,3),(1,4),(2,3),(2,4)]
  • liftA2 gives us a more terse way of expressing the above
Prelude> liftA2 (,) [1, 2] [3, 4]
[(1,3),(1,4),(2,3),(2,4)]
  • Similarly
Prelude> (+) <$> [1, 2] <*> [3, 5]
[4,6,5,7]
Prelude> liftA2 (+) [1, 2] [3, 5]
[4,6,5,7]
Prelude> max <$> [1, 2] <*> [1, 4]
[1,4,2,4]
Prelude> liftA2 max [1, 2] [1, 4]
[1,4,2,4]

Applicative laws

  • Identity

pure id <*> v = v

  • Composition

pure (.) <*> u <*> v <*> w = u <*> (v <*> w)

  • Homomorphism

pure f <*> pure x = pure (f x)

ie:

pure (+1) <*> pure 1
pure ((+1) 1)
  • Interchange

u <*> pure y = pure ($ y) <*> u

Monad

class Applicative m => Monad m where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  • Note the chain of dependency between functors, applicatives and monads: Functor -> Applicative -> Monad
  • In other words, functor and applicative can be derived from a monad, one law demonstrating this is: fmap f xs = xs >>= return . f
  • return is the same as pure, it simply lifts a value into context
  • >> aka Mr. Pointy aka the sequencing operator sequences two actions while dismissing the result
  • >>= is the bind operator, and it is what makes monads special
  • Let's reflect on the similarities between the key operations between functors, applicatives and monads:
fmap :: Functor f => (a -> b) -> f a -> f b
<*> :: Applicative f => f (a -> b) -> f a -> f b
>>= :: Monad f => f a -> (a -> f b) -> f b
  • To develop and intuition for the essence of monads,let's take an example using map on a function of type (a -> f b)
fmap :: Functor f => (a -> f b) ->  a -> f (f b)
  • Seeing the above with lists:
Prelude> andOne x = [x, 1]
Prelude> andOne 10
[10,1]
Prelude> :t fmap andOne [4, 5, 6]
fmap andOne [4, 5, 6] :: Num t => [[t]]
Prelude> fmap andOne [4, 5, 6]
[[4,1],[5,1],[6,1]]
  • Noting that now we have a list of lists, how do we flatten it? We use concat
Prelude> concat $ fmap andOne [4, 5, 6]
[4,1,5,1,6,1]
  • Monad in ways is the generalization of the concat, which leads us to the monad's join function:
join :: Monad m => m (m a) -> m a
-- compare
concat :: [[a]] -> [a]
  • Note that the join is what sets monads apart, this ability to peel away the extra embedded structure that is of the same type as the outer structure

  • Seeing the type signature for bind, we also note that bind is simply fmap composed with join:

-- keep in mind this is >>= flipped
bind :: Monad m => (a -> m b) -> m a -> m b
bind = join . fmap
  • Note that the do syntax in Haskell is sugar for sequencing and binding monads
sequencing :: IO ()
sequencing = do
  putStrLn "blah"
  putStrLn "another thing"

sequencing' :: IO ()
sequencing' =
  putStrLn "blah" >> putStrLn "another thing"

binding :: IO ()
binding = do
  name <- getLine
  putStrLn name

binding' :: IO ()
binding' = getLine >>= putStrLn
  • Note how the do syntax helps avoid nesting
  • The style is also reminiscent of imperative programming, but we are still programming in a purely functional way
bindingAndSequencing :: IO ()
bindingAndSequencing = do
  putStrLn "name pls:"
  name <- getLine
  putStrLn ("y hello thar: " ++ name)

bindingAndSequencing' :: IO ()
bindingAndSequencing' =
  putStrLn "name pls:" >> getLine >>=
    \name -> putStrLn ("y helo thar: " ++ name)

Laws

  • Identity: return should not perform an operations
-- right identity
m >>= return = m
-- left identity
return x >>= f = f x
  • Associativity: (m >>= f) >>= g = m >>= (\x-> f x >>= g)

Composition

  • To compose monads, we need Kleisli Composition
(.) :: (b -> c) -> (a -> b) -> a -> c
(>>=) :: Monad m => m a -> (a -> m b) -> m b
-- the order is flipped to match >>=
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
flip (.) :: (a -> b) -> (b -> c) -> a -> c
  • Taking an example
import Control.Monad ((>=>))
sayHi :: String -> IO String
sayHi greeting = do
  putStrLn greeting
  getLine
readM :: Read a => String -> IO a
readM = return . read
getAge :: String -> IO Int
getAge = sayHi >=> readM
askForAge :: IO Int
askForAge =
  getAge "Hello! How old are you? "

Foldable

  • Back to TOC

  • Chapter exercises

  • A list fold reduces the values inside a list to a summary value

  • This is done by recursively applying some function

  • The folding function is always dependent on some Monoid instances

  • Seeing the class definition, a Foldable instance must define either foldMap or foldr to be minimally complete

class Foldable t where
{-# MINIMAL foldMap | foldr #-}
  • Also, note: class Foldable (t :: * -> *) where, indicating t must be a higher kinded type
  • Seeing the overall class definition:
class Foldable (t :: * -> *) where
  fold :: Monoid m => t m -> m
  foldMap :: Monoid m=> (a -> m) -> t a -> m
  • fold allows us to combine elements inside a Foldable structure using the defined Monoids
  • foldMap on the other hand first maps each element of the structure to a Monoid, and then combines the results
  • Note the following examples to observe the differences b/w foldr, fold and foldMap
Prelude> foldr (+) 0 [1..5]
15
Prelude> fold (+) [1, 2, 3, 4, 5]
-- error message resulting from incorrect
-- number of arguments

Prelude> xs = map Sum [1..5]
Prelude> fold xs
Sum {getSum = 15}


Prelude> :{
*Main| let xs :: [Sum Integer]
*Main|     xs = [1, 2, 3, 4, 5]
*Main| :}
Prelude> fold xs
Sum {getSum = 15}
  • In some cases, the compiler can figure out the monoid instance and doesn't need the explicit typing, for example with strings:
Prelude> foldr (++) "" ["hello", " julie"]
"hello julie"
  • Similarly, one can use a foldMap to specify the function that will convert the elements to something with a non ambiguous monoid instance
Prelude> foldMap Sum [1, 2, 3, 4]
Sum {getSum = 10}
Prelude> foldMap Product [1, 2, 3, 4]
Product {getProduct = 24}

  • Note that Sum and Product are the functions/data constructors that disambiguate the monoid instance for integers, but foldMap can also be used as follows:
Prelude> xs = map Sum [1..3]
Prelude> foldMap (*5) xs
Sum {getSum = 30}
-- 5 + 10 + 15
30
  • Let's look at some examples of Foldable instances
instance Foldable Identity where
  foldr f z (Identity x) = f x z
  foldl f z (Identity x) = f z x
  foldMap f (Identity x) = f x
Prelude> foldr (*) 1 (Identity 5)
5
Prelude> foldl (*) 5 (Identity 5)
25
-- Using a fake maybe type
instance Foldable Optional where
  foldr _ z Nada = z
  foldr f z (Yep x) = f x z
  foldl _ z Nada = z
  foldl f z (Yep x) = f z x
  foldMap _ Nada = mempty
  foldMap f (Yep a) = f a
Prelude> foldr (+) 1 Nothing
1

Prelude> fm = foldMap (+1)
Prelude> fm Nothing :: Sum Integer
Sum {getSum = 0}
  • Note that in the above cases, the fold functions are not used to combine values, but simply consume the values from within their structures

Traversable

  • Back to TOC

  • Chapter exercises

  • Functors transform values within a structure

  • Applicatives transform values with within a structure where the transformer function is also within a structure

  • Traversable allows us to process values embedded in a structure as if they existed in a sequential order

Traversable allows you to transform elements inside a structure like a functor, producing applicative effects along the way, and lift those potentially multiple instances of applicative structure outside of the traversable structure. It is commonly described as a way to traverse a data structure, mapping a function inside a structure while accumulating applicative contexts in the process.

  • Seeing the traversable type class definition:
class (Functor t, Foldable t) => Traversable t where
  traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
  traverse f = sequenceA . fmap f
  • traverse maps each element in a structure to an action, evaluates the actions left to right, and then collects the results
  • Comparing traverse to fmap
fmap :: Functor f => (a -> b) -> f a -> f b
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)

myData :: [String]
myFunc :: String -> IO Record
wrong :: [IO Record]
wrong = fmap myFunc myData
right :: IO [Record]
right = traverse myFunc myData

-traverses counterpart is sequenceA; note the minimal instance as traverse and sequenceA can be defined in terms of the other

-- | Evaluate each action in the
-- structure from left to right,
-- and collect the results.
sequenceA :: Applicative f => t (f a) -> f (t a)
sequenceA = traverse id
{-# MINIMAL traverse | sequenceA #-}
  • Let's see sequenceA in more detail
  • As seen in the type signature, sequenceA only flips two contexts or structure
Prelude> xs = [Just 1, Just 2, Just 3]
Prelude> sequenceA xs
Just [1,2,3]
Prelude> xsn = [Just 1, Just 2, Nothing]
Prelude> sequenceA xsn
Nothing
Prelude> fmap sum $ sequenceA xs
Just 6
Prelude> fmap product (sequenceA xsn)
Nothing
  • In the first two example, we are simply flipping around the list, and Maybe contexts

  • Note in the second set of examples, we can lift a function (sum in this case) into the Maybe context

  • Looking at traverse now, keeping in mind traverse f = sequenceA . fmap f

Prelude> fmap Just [1, 2, 3]
[Just 1,Just 2,Just 3]
Prelude> sequenceA $ fmap Just [1, 2, 3]
Just [1,2,3]
Prelude> sequenceA . fmap Just $ [1, 2, 3]
Just [1,2,3]
Prelude> traverse Just [1, 2, 3]
Just [1,2,3]
  • In general, we use traversable when we need to flip around two type constructors, or map a function and hen flip around the constructors
  • Note also that Traversable is stronger than Functor and Foldable, therefore, we can recover Functor and Foldable from Traversable, similar to how Functor and Applicative can be recovered from Monad
  • See the chapter exercises for examples of Traversable instances

Traversable laws

traverse must satisfy:

  • Naturality: t . traverse f = traverse (t . f)
  • Identity: traverse Identity = Identity
  • Composition: traverse (Compose . fmap g . f) = Compose . fmap (traverse g) . traverse f

sequenceA must satisfy:

  • Naturality: t . sequenceA = sequenceA . fmap t
  • Identity: sequenceA . fmap Identity = Identity
  • Composition: sequenceA . fmap Compose = Compose . fmap sequenceA . sequenceA

Monad Transformers

Note: this concept is very dense, and cannot be summarized effectively (at least not by me, yet anyways). Refer back to the book itself and other resources as needed. Following is a decent, terse overview: Monad Transformers Explained

  • The issue with monads, unlike functors and applicatives, is that one cannot guarantee getting a monad out of putting two other monads together
  • Hence, to when we need to compose two monads, we need monad transformers
  • The monad transformer is a type constructor that takes a Monad as an argument and returns a Monad as a result
  • The essence of the issue of composing two monads is that it is impossible to join two unknown monads.
  • To make the join happen, we need to reduce polymorphism an make one of the monads concrete. The other can stay as a variable
  • Note the IdentityT monad transformer as an example
  • Quoting the book: note the following, critical pattern
Transformers are bearers of single-type concrete information that let you create ever-bigger monads, in a sense. Nesting such as:

(Monad m) => m (m a)

Is addressed by join already. We use transformers when we want a >>= operation over f and g of different types (but both have Monad instances). You have to create new types called monad transformers and write Monad instances for those types to have a way of dealing with the extra structure generated.

The general pattern is this: You want to compose two polymorphic types, f and g, that each have a Monad instance. But you’ll end up with this pattern:

f (g (f b))

Monad’s bind can’t join those types, not with that intervening g. So you need to get to this:

f (f b)

You won’t be able to unless you have some way of folding the g in the middle. You can’t do that with Monad. The essence of Monad is join, but here you have only one bit of g structure, not g (g ...), so that’s not enough. The straightforward thing to do is to make g concrete. With concrete type information for the inner bit of structure, we can fold out the g and get on with it. The good news is that transformers don’t require f to be concrete; f can remain polymorphic, so long as it has a Monad instance, and therefore we only need to write a transformer once for each type.

About

Advancing functional programming knowledge through Haskell

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors