Since my last post I have been climbing what I recently heard Julie Moronuki call the Haskell Pyramid. I have been learning about the bread and butter abstractions which Haskell programmers use all the time and serve as useful tools to describe and compose programs. These are not only required knowledge to be able to use most of the really cool Haskell features that excite me (which I will cover in a later post) but also useful ways to model computation in general, even if one is not using Haskell.
I have been learning about SemiGroups, Monoids, Functors, Applicatives, Monads, Foldables, Traversables and Reader from the Haskell Book, which Julie co-authored. I figured that now would be a good time to take stock of what I have learned. This is meant to be a fly by of these topics, more of a cheatsheet than a tour. If you want to learn more about these topics, I recommend checking out Haskell Book. All the exercises I worked through to gain an intuition about these abstractions can be found here
SemiGroup, Monoid, Functor, Applicative, and Monad are abstractions, which have laws, which concrete implementations must conform to in order to be a valid instances of that abstraction. While learning about these abstractions, I used QuickCheck, a property based testing library, to verify that the instances I wrote conformed to the appropriate laws. I found this to be an extremely effective way to learn new concepts because my test suite was able to serve as a constant feedback loop.
As described on the Hackage SemiGroup package, a SemiGroup is an algebraic structure, consisting of a set together with an associative, binary operation (
I know this function
<> is called
mappend if on Monoid, but I don't know
what it is called on SemiGroup.
In order for something to be a valid SemiGroup it's
<> function must be binary
(take two arguments) and associative:
a <> (b <> c) == (a <> b) <> c
Some things that exhibit SemiGroup behavior include non empty lists, and getting
the first and last elements from a sequence of (
First 1 <> First 2 <> First 3 => First 1
Last 1 <> Last 2 <> Last 3 => Last 3
Monoids are a SemiGroup that has an identity value (described by
mempty) such that:
mempty <> x == x x <> mempty == x
All Monoids also have the binary associative function
<> which can be inherited
from their SemiGroup instance, and is called
<> == mappend if it is
being applied to a Monoid).
Numbers (both over multiplication and addition) boolean conjunction (
a && b && c) and (
a || b || c) patterns, list concatenation
and zipping all exhibit Monoidal behavior (i.e. they have an identity value and
a binary associative operation).
Lucy Zhang also showed me that Groups (which I came across when researching Kademlia, the DHT protocol used in BitTorrent and IPFS) are Monoids that have an inverse for every value. An example of groups would be integers over addition, where zero is it's own inverse, and negative numbers and positive numbers are inverses of each other.
Monoids have kind
Functors have some structure that is not able to be altered, but which can allow for transformations on values that are not part of the structure. List, Maybe, Either, Tuples, and functions are all functors.
fmap, which can also be written as
fmap :: Functor f => (a -> b) -> f a -> f b
and have the following laws which they must obey to preserve their structure:
fmap id == id
fmap (f . g) == fmap f . fmap g
If you try to change the structure of a functor it will violate the composition
law, as applying
fmap multiple times will yield a different result, than just
applying it once and composing two functions.
Functors must have kind
* -> * as they must have a type inhabitant which is not
part of the structure, such as the
Right values in the case of
list elements in the case of Lists .
Lists, functions, and
IO are all examples of functors. Function composition,
fmap on functions:
(.) :: (b -> c) -> (a -> b) -> (a -> c)
Something I found profoundly beautiful is when one applies
fmap to itself (as
in the example below)
(fmap . fmap) show [Just 1, Just 3, Nothing] => [Just "1", Just "3", Nothing]
the way the types work out requires algebraic substitution of the types:
(.) :: (b -> c) -> (a -> b) -> (a -> c) fmap1 :: Functor f => (a -> b) -> f a -> f b fmap2 :: Functor g => (c -> d) -> g c -> g d
fmap . fmap the type signature expands out to
((a -> b) -> (f a -> f b)) -> ((c -> d) -> (g c -> g d))
Because we know that the
b in compose
(b -> c) -> (a -> b) -> (a -> c)
is the same as:
(a -> b)
in the first fmap, and
(g c -> g d)
in the second fmap
we know that
f a from the first fmap can be rewritten as
f (g c) and
f b from the second can be rewritten as
f (g d),
which means we can rewrite our type signature for
fmap . fmap as
(c -> d) -> (f (g c) -> f (g d))
which describes at the type level transforming a value within a functor, within a functor.
It absolutely blew my mind that you could work that out from the types alone without needing to think about what is happening at the value level at all.
Left values are ignored by
fmap because they are
part of the structure, which must be left intact in order to comply with the functor
Functor (unlike Monoid) only has a single valid instance for a given type.
Functors are very useful for transforming values inside of
IO. I used
this repeatedly in my QuickCheck testing to transform lists generated by QuickCheck
into other data-structures which I needed for testing.
As described in the Haskell Book:
"Applicatives are monoidal functors:
- Monoids give us a way to mash data-structures together.
- Functors give us a way do do function application in a structure.
- Applicatives give us away to apply functions which are themselves in a structure over another structure and then mash the structures together."
Applicatives have 2 operations:
pure :: Applicative f => a -> f a
which takes a value of any type and puts it into an applicative context
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
which applies a function in an applicative structure to values in an applicative structure, and combines the two applicative structures.
They must together follow the following laws:
pure id <*> v = v
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
pure f <*> pure x = pure (f x)
u <*> pure y = pure ($ y) <*> u
If we compare,
$ (which is just function application),
$ ::(a -> b) -> a -> b <$> ::(a -> b) -> f a -> f b <*> ::(a -> b) -> f a -> f b
we see that
fmap is jut function application over a functor, and applicative
application is just functorial application, where the function is within a
structure, which implies that the structures must be combined in some way.
<*> lets us do function application on functions in a structure like so:
Just (*2) <*> Just 3 => Just 6
Nothing <*> Just 3 => Nothing
Wrapping a function in
pure and applying
<*> is the same as just fmapping using
pure f <*> y == f <$> y
Applicatives have their structure combined using a monoidal pattern, however because there are multiple possible monoidal patterns on many types, there is no guarantee the applicative will use any specific Monoid instance for that type. Also, there is no typeclass constraint on Applicatives that they must be Monoids.
The function application component will be the same as
fmap though, as there
is only one valid functorial instance for a given type.
As Nick Collins explained during our
Haskell Study group, Monad's bind (
>>=) is nothing more than flatMap.
class Applicative m => Monad m where (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b return :: a -> m a
I also learned about the fish operator:
(>=>) Monad m -> (a -> m b) -> (b -> m c) -> a -> m c
I don't have much to say about monads other than if you just treat them like things which support flatMap (whereby you are able to flatten out structures generated from calling bind, while adding new resulting values to the new structure) I find that they become a lot easier to work with and reason about.
Foldable (which is a typeclass which allows your
data-structure to be folded over), requires only either
foldr or or
foldMap to be defined. If you define
you need for the type to also have a Monoid instance so you are able to use
mconcat to summarize the Monoid structures you are folding over.
By defining a foldable instance for your type, you are also able to get a number of convenience functions which can be written using folding functions inferred automatically including:
foldl' :: (b -> a -> b) -> b -> t a -> b toList :: t a -> [a] null :: t a -> Bool length :: t a -> Int elem :: Eq a => a -> t a -> Bool maximum :: forall a. Ord a => t a -> a minimum :: forall a. Ord a => t a -> a sum :: Num a => t a -> a product :: Num a => t a -> a
While learning about Foldable with Santiago Gepigon
during the Haskell Study Group, we also learned about the
language extension and how it is needed to define type signatures for
polymorphic functions defined in where clauses.
As described in the book:
"Traversable allows you to transform elements in a structure (like functor) while producing applicative effects along the way, and lift the (potentially multiple) applicative effects outside of the traversable structure."
Has 2 functions:
sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a)
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
Here are some examples of how they work:
sequenceA [Just 1, Just 2, Just 3] => Just [1,2,3] sequenceA [Just 1, Just 2, Nothing] => Nothing sequenceA :: (Traversable t, Applicative f) => -> t (f a) -> f (t a) traverse :: (Traversable t, Applicative f) => (a -> f a) -> t a -> f (t a)
traverse must follow the following laws:
t . traverse f = traverse (t . f)
traverse Identity = Identity
traverse (Compose . fmap g . f) = Compose . fmap (traverse g) . traverse f
sequenceA must follow the following laws:
t . sequenceA = sequenceA . fmap t
sequenceA . fmap Identity = Identity
sequenceA . fmap Compose = Compose . fmap sequenceA . sequenceA
Traverse is really useful when you have a traversable and you need to transform
it into an applicative of a traversable, while converting each element to an
applicative and applying
<*> to the intermediate values.
As mentioned above, functions (
(->) r) are functors and
(.) == fmap. The
r is part of the functoral
structure, and therefor the only thing that can change with
fmap application is
the return value of the function.
Reader r a) is just a wrapper around function composition.
Reader here is part of the
f structure, so it gets applied with the first argument:
pure :: a -> f a pure :: a -> (r -> a) (<*>) :: f (a -> b) -> f a -> f b (<*>) :: (r -> a -> b) -> (r -> a) -> (r -> b)