Warning: Advanced Mathematics Ahead
These contents are not taught in other tutorials, but they can greatly increase your understanding of functors and monads! They also contain many of my personally devised insights.
Review the concept of sets. In the category of sets, we not only discuss sets
Sets can be seen as a collection of objects gathered together. However, sets themselves do not remember the relationships between these objects. Categories, on the other hand, can be seen as an “upgraded” version of sets (strictly speaking, the objects in a category do not need to form a set, this is a small category).
We say that a category
It contains a class of objects
For every two objects
For every three objects
This composition satisfies the associative law, and
Example:
The universal set and all set-theoretic functions between sets form a category
The universal
In Haskell, all basic types, user-defined types, and the types formed by combining them, together with the functions that can be defined between these types, form a category
You can think of categories as an upgraded version of sets, where they not only remember a class of objects, but also remember the relationships between these objects and their compositions.
Since the sets
A mapping between sets only needs to correspond any object
However, objects in a category are connected by arrows, so a functor not only needs to correspond objects
And this correspondence process cannot break the composition rule of categories
If
If
You can think of a functor as an upgraded version of a mapping, which not only maps elements to elements, but also maps relationships to relationships.
We can forget the structure of a vector space and reduce it to a set. This correspondence is a functor from
The process of mapping any type
Similarly, the process of mapping a type
maybeMap :: (a -> b) -> Maybe a -> Maybe b
Nothing = Nothing
maybeMap f Just a) = Just (f a) maybeMap f (
It obviously satisfies the law of composition
. g) = maybeMap f . maybeMap g
maybeMap (f -- both sides are Nothing if input is Nothing.
-- both sides are Just (f (g a)) if input is Just a
Hom functor.
Hom functor in Haskell. The previous example appears exactly the same in Haskell, except that we are considering the functor from
homMap :: (a -> b) -> (c -> a) -> (c -> b) -- here the functor is (c ->)
= (.) -- if you use eta reduction
homMap -- homMap f g = f . g
In fact, in Haskell, these Type Constructors are all functors: IO, Maybe, [], Data.Map, (a ->)… The following Functor class is defined in the Haskell standard library: (These functors are all covariant functors)
class Functor f where
fmap :: (a -> b) -> f a -> f b
<$>) = fmap -- the functor operator, they are identical functions. (
This allows you to use these different functors with just fmap.
safeSqrt :: Double -> Maybe Double
*10) :: Double -> Double -- well, in fact (Num a) => a -> a
(fmap (*10) :: Maybe Double -> Maybe Double -- well, in fact, (Functor f, Num a) => f a -> f a
fmap (*10) . safeSqrt :: Double -> Maybe Double
checkValidPassword :: String -> Maybe String
getLine :: IO String
```Haskell
-- in the number guessing game, we used some functor magic
readMaybe input :: Maybe Integer
compare :: (Ord a) => a -> a -> Ordering
compare secretNumber :: Integer -> Ordering
fmap (compare secretNumber) :: Maybe Integer -> Maybe Ordering
In the number guessing game, we used some functor magic. readMaybe input :: Maybe Integer
is the same as compare :: (Ord a) => a -> a -> Ordering
. compare secretNumber :: Integer -> Ordering
is the same as fmap (compare secretNumber) :: Maybe Integer -> Maybe Ordering
.
The set of all functors from category
In Haskell, a natural transformation between functors f and g should be a family of mappings from f a to g a, defined for all a (unrestricted polymorphic function).
natural :: FunctorF a -> FunctorG a
-- examples abound:
repeat :: a -> [a]
-- this is a natural transform from 1_Types (identity functor) to [] (list functor).
tail :: [a] -> [a]
-- A natural transformation from the list functor [] to itself.
const :: a -> (b -> a)
-- A natural transformation from 1_Types to (b ->)
Monad is a special functor
Therefore, we say that
In Haskell, the definition of Monad requires two polymorphic functions on type a without restrictions (thus, they are natural transformations of functors on the category Types).
join :: (Monad m) => m (m a) -> m a -- mu, the multiplication of the monad.
return :: (Monad m) => a -> m a -- eta, unit of the monad
However, in Haskell, the definition of Monad is not the multiplication (join) mentioned above, but a binding operator (>>=).
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
It can be proven that giving join and giving (>>=) are equivalent, we can use the functor property to turn
>>= f = join $ fmap f ma
ma -- or...
>>=) = flip $ (join .) . fmap -- I spend 5 minutes trying to write this
(-- equivalently,
>>=) = fmap join . flip fmap
(-- Challenge : can you understand these two expressions?
-- Hint:
-- fmap :: (a -> m b) -> m a -> m m b
-- join :: m m b -> m b
-- ((join .) .) :: (d -> c -> m m b) -> (d -> c -> m b)
-- (join .) . fmap :: (a -> m b) -> m a -> m b
-- flip is a standard function that swaps the two arguments of a two-variable function
In turn, if we are given (>>=), we can obtain multiplication
m (m a) -> (m a -> m a) -> m a
The mapping m a -> m a in the middle can be taken as the identity mapping.
The Kleisli category is defined as the function join
which is equivalent to the function >>= id
in Haskell.
When a Monad
Composition of arrows is given by the following method: if we have
This is an arrow of
In other words, we have a function
(<=<) :: (Monad m) => (b -> m c) -> (a -> m b) -> a -> m c
-- the composition of Kleisli Arrows
-- Challenge : can you write the definition of (<=<) using join, in a pointless way?
-- we also have an opposite arrow
(>=>) :: (Monad m) => (a -> m b) -> (b -> m c) -> a -> m c
newtype IO a = IO a -- you are not allowed to use this constructor, though
- IO a represents an action on the external world,
- when completed, will return a value of type a
instance Functor IO where
fmap fab (IO a) = -- wait for IO a to return a value a, and then apply fab
This multiplication obviously satisfies the associative law, and its unit element is
It wraps a value of type
After defining this multiplication and unit, we can obtain that IO is a Monad.
Similarly, we notice that two possibly failing actions can be combined into one possibly failing action
You can think of Maybe a as a “Schrödinger’s box”, which may contain an object of type a (Just a), or it may contain nothing (Nothing). When we have two of these boxes, one nested inside the other, we can combine them into one equivalent box. When either layer is Nothing, we consider the box to give us Nothing, and when both layers give us Just (Just a), we consider the box to give us Just a. This way, we combine the two layers of Maybe into one.
join :: Maybe (Maybe a) -> Maybe a
Nothing) = Nothing
join (Just Nothing) = Nothing
join (Just (Just a)) = Just a
join (
maybe >>= klei
= join $ fmap klei maybe
= case maybe of
Just a -> klei a
Nothing -> Nothing
The unit of multiplication is a mapping that does nothing but put a determined value into a box:
return :: a -> Maybe a
return x = Just x
Two layers of
join :: [[a]] -> [a]
= concat
join
>>= klei
list = join $ fmap klei list
= concat (map klei list)
Its unit is a function that wraps a value into a single-element list
return :: a -> [a]
return x = [x]
The State Monad is a type of monad that allows for stateful computations. It is commonly used in functional programming languages to manage and manipulate state within a program.
The Either Monad is a type of monad that represents computations that can result in either a successful value or an error. It is often used to handle error handling and branching in functional programming.
The do-notation is a syntax for writing imperative-style code in a functional programming language. It allows for a more readable and intuitive way of working with monads, such as the State Monad and Either Monad.
The multiplication and unit of Monad allow us to express many sequential calculations and compositions. Thanks to the associativity, we can also use the concise and vivid do-notation to manage the composition of Monad.
main :: IO ()
= getLine >>= (\input -> putStrLn input) -- or just getLine >>= putStrLn
main
-- equivalent way in do-notation
= do
main <- getLine -- do notation is a syntax sugar, it is equivalent to the above
input putStrLn input
= do
main putStrLn "name?"
<- getLine
name putStrLn "age?"
<- getLine
age putStrLn $ "Your name and age is" ++ show (name, age)
-- This code is equivalent to:
= putStrLn "name?" >>=
main -> getLine >>=
(\_ -> putStrLn "age?" >>=
(\name -> getLine >>=
(\_ -> putStrLn ("Your name and age is " ++ show (name, age))
(\age
)
)
) )
The code above prompts the user for their name and age, and then prints out a message with their name and age. The do
notation is used to make the code more readable and easier to understand. However, it can be rewritten using the >>=
operator, which is used to chain together multiple actions. Each action is executed sequentially, with the result of the previous action being passed as an argument to the next action. This allows for a more concise and functional style of programming.
Monad is a typeclass in Haskell that represents computations that can be sequenced and composed. It is defined by the following functions:
return :: a -> m a
- takes a value and wraps it in the monad type constructor.>>= :: m a -> (a -> m b) -> m b
- takes a monad value and a function that takes a normal value and returns a monad value, and applies the function to the value inside the monad.>> :: m a -> m b -> m b
- takes two monad values and returns the second one, discarding the first one.fail :: String -> m a
- takes a string and returns a monad value that represents a failure.The return
function is used to lift a value into a monad, while the >>=
function is used to sequence computations. The >>
function is similar to >>=
, but it discards the result of the first computation. The fail
function is used for error handling in monads.
Monads must also follow three laws: left identity, right identity, and associativity. These laws ensure that monads behave predictably and can be composed in a consistent manner.
We have clearly described the mathematical definition of Monad, which is equivalent to giving join and return in Haskell, and is also equivalent to giving (>>=) and return. However, if you want to define a Monad m in Haskell, Haskell requires you to first implement the Functor class for m (as expected) and the Applicative class (a new concept!).
class Functor f => Applicative f where
pure :: a -> f a -- this is just the unit, or 'return'
(<*>) :: f (a -> b) -> f a -> f b -- this allows you to apply a function value as a functor
Let’s first explain why a Monad naturally becomes an Applicative functor.
The <*>
function takes two monadic values, mf
and ma
, and applies the function f
from mf
to the value a
from ma
, resulting in a new monadic value mb
. This can be written using the do
notation as follows:
(<*>) :: m (a -> b) -> m a -> m b
<*> ma = do
mf <- mf
f <- ma
a return $ f a
Alternatively, it can be written using the >>=
operator and lambda expressions:
<*> ma = mf >>= (\f -> ma >>= (\a -> return (f a))) mf
Therefore, Monads are all Applicatives, but not all Applicatives are Monads. Therefore, Applicative is a weaker concept than Monad in Haskell.
instance Functor Maybe where
fmap f Nothing = Nothing
fmap f (Just a) = f a
The Maybe type is an instance of the Applicative and Monad typeclasses. The pure function takes a value and wraps it in a Just constructor. The <*> operator takes a Maybe value containing a function and a Maybe value containing a value, and applies the function to the value, returning a new Maybe value. If either of the Maybe values is Nothing, the result is also Nothing.
The return function is the same as the pure function. The >>= operator takes a Maybe value and a function that takes a value and returns a Maybe value. If the Maybe value is Just, the function is applied to the value and the result is returned. If the Maybe value is Nothing, the result is also Nothing.
myCalculation :: String -> Maybe Double
= do
myCalculation input <- readMaybe input
x <- safeSqrt x
y <- safeSqrt (1 - y)
z return z
Category is an upgraded version of a set, which not only remembers the elements, but also remembers the arrows between these elements and the composition of arrows.
Functor is an upgraded mapping, which not only corresponds to elements, but also corresponds to arrows and maintains the composition of arrows. In Haskell, the functor mapping is unified by fmap or <$>.
Monad is a special endofunctor, which comes with two natural transformations, multiplication
IO, Maybe, [], Data.Map, (a ->) are all examples of functors in Haskell.
IO, Maybe, [] are all examples of monads in Haskell.
In Haskell, monads naturally become applicative functors, and in Haskell you need to define applicative before defining monad.