Warning: 硬核数学ahead
这些内容是其他教学不会教的,但是能大大增加你对函子和Monad的理解!里面也包含了不少我个人想出来的精华理解哦。
回顾集合的概念。在集合的范畴中,我们不仅讨论集合
集合可以视作一类物件聚集在一起。但是集合本身并不会记住这些物件之间的关系。而范畴则可以看成一个“升级版”的集合(严格来讲范畴中的物件不需要构成一个集合,这是小范畴)。
我们说一个范畴(Category)
里面有一类物件
对于每两个物件
对于每三个物件
这个合成满足结合律,
举例:
全体集合,以及集合之间的所有集合论函数,构成一个范畴
全体
Haskell中全体基本类型,用户自定义类型,以及它们组合出来的类型,加上这些类型之间可以定义的函数,构成一个范畴
你可以简单理解为:范畴就是一个升级版的集合,它不光记住了一类物件,它还记住了这些物件之间的关系以及这些关系的复合。
既然集合
一个集合间的映射只需要将
但是范畴中的物件是有箭头将它们联系起来的,因此一个函子不仅要把
并且这个对应过程不能破坏范畴的复合规则
如果
如果
你可以将函子理解为一个升级版的映射,它不光把元素对应到元素,还把关系对应到关系。
我们可以将任何向量空间忘记掉向量空间的结构,退化成集合.这个对应关系是一个从
把
类似的,把类型
maybeMap :: (a -> b) -> Maybe a -> Maybe b
Nothing = Nothing
maybeMap f Just a) = Just (f a) maybeMap f (
它显然满足合成律
. 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函子。
Haskell中的Hom函子。上一个例子也完全相同的出现在Haskell中,不同的是我们考虑的是
homMap :: (a -> b) -> (c -> a) -> (c -> b) -- here the functor is (c ->)
= (.) -- if you use eta reduction
homMap -- homMap f g = f . g
事实上,在Haskell中,这些Type Constructor都是函子:IO, Maybe, [], Data.Map, (a ->)… 在Haskell标准库中定义了如下Functor class: (这些函子都是协变函子)
class Functor f where
fmap :: (a -> b) -> f a -> f b
<$>) = fmap -- the functor operator, they are identical functions. (
这使得你只需要使用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
lengthPassword :: String -> Maybe Int
= fmap length (checkValidPassword input)
lengthPassword input -- = length <$> (checkValidPassword input)
getLine :: IO String
fmap length getLine :: IO Int -- length :: String -> Int
length <$> getLine :: IO Int -- they are identical
-- in the number guessing game, we used some functor magic
input :: Maybe Integer
readMaybecompare :: (Ord a) => a -> a -> Ordering
compare secretNumber :: Integer -> Ordering
fmap (compare secretNumber) :: Maybe Integer -> Maybe Ordering
compare secretNumber <$> readMaybe input :: Maybe Ordering
值得注意的是,所有从范畴
在Haskell中,函子f, g之间的自然变换应该是一族映射 f a -> g a, 对所有a都有定义(无限制的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 transform from the list functor [] to itself.
const :: a -> (b -> a)
-- a natural transform from 1_Types to (b ->)
Monad 是一种从一个范畴到它自身的特殊的函子
那么,我们就说
用Haskell的语言来说,Monad的定义需要两个对a的类型无限制的Polymorphic函数(从而是范畴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
但是在Haskell中,大家对Monad的定义不是上述乘法(join),而是一个连接运算符(>>=)
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
可以证明,给出join和给出(>>=)是等价的,我们可以使用函子属性将
>>= 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
反过来,如果给出了(>>=), 我们可以得到乘法
m (m a) -> (m a -> m a) -> m a
中间的映射m a -> m a 取恒等映射即可。
= (>>= id) join
当
箭头的合成由下面的方法给出,如果我们有
这就是一个
也就是说我们得到了一个函数
(<=<) :: (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 return an value a , and then apply fab
IO 是一个Monad, 因为IO (IO a) 可以合成为一个IO a. 你可以想象一下,一个执行后会返回一个“执行后会返回a的动作”的动作,可以视为一个单独的,执行后会返回a的动作。这个过程是将两个动作合成为一个动作,这便是IO的乘法。
这个乘法显然满足结合律,它的单位元是
将一个类型为
定义了这个乘法和单位之后,我们就可以得到IO 是一个Monad.
类似的,我们注意到两次可能失败的动作可以合成一个可能失败的动作
你可以把Maybe a 想象成一个“薛定鄂”的盒子,里面可能装着类型a的物件(Just a),也可能什么也没有(Nothing)。当我们有两个这样的盒子,一个套在另一个盒子外面的时候,我们可以把这两个盒子等效的合成为一个。当打开这两层的任意一层是Nothing的时候,我们就视为这个盒子给出了Nothing, 而两层都给出Just(Just a)时,我们就视为这个盒子给出了Just a. 这样我们就将两层Maybe 合为了一层。
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
乘法的单位是如下什么也不干,将一个确定的值装进盒子的映射:
return :: a -> Maybe a
return x = Just x
两层
join :: [[a]] -> [a]
= concat
join
>>= klei
list = join $ fmap klei list
= concat (map klei list)
它的单位是把值包装为一个单元素的list的函数
return :: a -> [a]
return x = [x]
Monad 的乘法和单位允许我们表达很多顺序计算和合成。由于结合律,我们还可以使用简洁又形象的do-notation来管理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)
-- what the above really means is:
= putStrLn "name?" >>=
main -> getLine >>=
(\_ -> putStrLn "age?" >>=
(\name -> getLine >>=
(\_ -> putStrLn ("Your name and age is " ++ show (name, age))
(\age
)
)
)
)
-- Luckily due to the associativity law of monad
-- (ma >>= famb) >>= fbmc = ma >>= (\a -> famb a >>= fbmc)
-- we do not have to write these parenthesis,
-- i.e.
putStrLn "name?" >>= ( \_ -> getLine >>= \name -> something )
= -- associativity law of monad guarantees that these are equal
putStrLn "name?" >>= \_ -> getLine ) >>= \name -> something
( -- and do-notation is well defined without specifying any parenthesis.
我们已经清楚的叙述了Monad的数学定义,Haskell中等价于给出 join 和 return, 也等价与给出 (>>=)和return. 但如果你需要在Haskell中定义Monad m, Haskell要求你先为m实现Functor class (这和我们预期的一致)和Applicative class(新概念!).
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
我们先来说明一个Monad为什么自然的会成为一个Applicative函子.
(<*>) :: m (a -> b) -> m a -> m b
<*> ma = do
mf <- mf
f <- ma
a return $ f a
-- or
<*> ma = mf >>= (\f -> ma >>= (\a -> return (f a))) mf
于是,Monad都是Applicative的,但是Applicative的则不一定是Monad.因此Applicative在Haskell中是一个比Monad要弱的概念。
instance Functor Maybe where
fmap f Nothing = Nothing
fmap f (Just a) = f a
instance Applicative Maybe where
pure = Just
<*> ma = case mf of
mf Just f -> case ma of
Just a -> Just (f a)
Nothing -> Nothing
Nothing -> Nothing
instance Monad Maybe where
return = pure
>>= klei = case ma of
ma Just a -> klei a
Nothing -> Nothing
-- Now you can use do-notation to sequence chains of Maybe applications!
myCalculation :: String -> Maybe Double
= do
myCalculation input <- readMaybe input
x <- safeSqrt x
y <- safeSqrt (1 - y)
z return z
范畴是一种升级版的集合,它不仅记住了有哪些元素,还记住了这些元素之间的箭头和箭头的合成。
函子是一种升级的映射,它不仅要对应元素,还要对应箭头并且保持箭头之间的合成。Haskell中的函子映射由fmap 或者<$>统一给出。
Monad是一种特殊的自函子,它附带了两个自然变换,乘法
IO, Maybe, [], Data.Map, (a ->) 都是Haskell中函子的例子。
IO, Maybe, [] 都是Haskell中Monad的例子。
在Haskell中,Monad自然的成为Applicative函子,并且在Haskell中你需要先定义Applicative才能定义Monad.