一.Functor像盒子?
盒子的比喻
常见的Functor类实例似乎都可以比作盒子(或者叫容器),比如Maybe/Either
,List([]
):
> fmap (+1) (Just 3)
Just 4
> fmap (+1) (Right 3)
Right 4
> fmap (+1) [1, 2, 3]
[2,3,4]
通过fmap
把函数作用于容器里的值,得到一个装着新值的同类容器,甚至I/O Action也可以这样理解:
> fmap (++"!") getLine
abc
"abc!"
I/O Action类容器特殊之处在于,容器里的值是不确定的,取决于外部输入,可能来自用户键入、文件读取、甚至直接从系统环境取(比如随机数种子)。但可以肯定的是,I/O Action这个容器里装着一个值(不论这个值来自哪里),而fmap
能够把函数作用于这个值,同样得到一个装着新值的I/O Action
至此,盒子的比喻仍然很恰当:纯环境下的容器是木质宝箱,里面装着确定不变的东西,而不纯环境下的容器是食人宝箱,里面不知道装着个啥。如下图(六一刚过,调皮一下):
函数也是Functor类实例?!
那么,是不是所有的Functor类实例都可以这样理解呢?
instance Functor (Const m) -- Defined in ‘Data.Functor.Const’
instance Functor (Either a) -- Defined in ‘Data.Either’
instance Functor [] -- Defined in ‘GHC.Base’
instance Functor Maybe -- Defined in ‘GHC.Base’
instance Functor IO -- Defined in ‘GHC.Base’
instance Functor ((->) r) -- Defined in ‘GHC.Base’
instance Functor ((,) a) -- Defined in ‘GHC.Base’
(注意:简单起见,上面列出的只是一般Functor实例,去掉了3个同样属于Applicative的特殊Functor实例)
其它几个都没什么特别的,((->) r)
这个东西长得有点奇怪,看起来像函数定义(r map to something),看下定义:
instance Functor ((->) r) where
fmap = (.)
((->) r)
确实是Functor
类实例,实现的fmap
就是函数组合(.
):
(.) :: (b -> c) -> (a -> b) -> a -> c
接受一个map b to c的函数和一个map a to b的函数,把后者的输出连接到前者的输入,返回map a to c的函数。这是我们所熟知的函数组合,但又与Functor有什么关系?
首先Functor
的fmap
类型是:
fmap :: Functor f => (a -> b) -> f a -> f b
既然((->) r)
也是Functor
实例,用((->) r)
换掉f
:
fmap :: (a -> b) -> (->) r a -> (->) r b
最后把->
换成习惯的中缀形式:
fmap :: (a -> b) -> (r -> a) -> (r -> b)
这,不就是函数组合(.
)吗?
(.) :: (b -> c) -> (a -> b) -> a -> c
所以,函数也是Functor
类实例
P.S.那么,((->) r)
为什么长得这么奇怪?因为Functor class要求:
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
f
必须是接受一个具体类型参数的类型(* -> *
),而->
是:
(->) :: * -> * -> *
多了个参数,所以先填一个,就得到丑丑的(->) r
了(r
只是个形参名,叫a
叫b
都行)
更恰当的比喻
函数,确实很难被想成是盒子。想象力实在丰富的话,可以想作生化盒子(魔斯拉),或者坩埚(女巫森林一张新卡)之类的能让内容发生变化的盒子,嗯,试管
函数层面的fmap
就是函数组合,对着map a to b的函数,做一发map b to c的映射,得到一个map a to c的新函数:
instance Functor ((->) r) where
fmap = (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
对比之前盒子的比喻:
通过
fmap
把函数作用于容器里的值,得到一个装着新值的同类容器
代入我们发明的生化盒子,得到:通过fmap
把(生化)盒子作用于(生化)盒子,得到一个新(生化)盒子
这3个“(生化)盒子”要怎么理解?
把map a to b和map b to c当做两根试管,并且如果试管能连接的话,勉强说得通:
-- 试管ab能把水变红
a -> b
-- 试管bc能把红水变蓝
b -> c
-- 拿试管bc对ab做映射,就是把ab的底戳个洞,套进bc试管
(b -> c) . (a -> b)
-- 得到一根(更长的)新试管ac,作用是把水变蓝
a -> c
为什么比作试管(或者生化盒子)?因为代指一种转换,想要表达变化。而我们所理解的盒子,缺少这种具有转换作用的含义,因此这个比喻不恰当
所以,对于函数上下文的Functor
盒子的比喻不是那么恰当,functors其实比较像computation。function被map over到一个computation会产生经由那个function映射过后的computation
上面这个描述相当贴切了,computation就是数据转换,而转换是能做映射的,做映射的方式就是组合
一个比较正确的形容是functors是一个计算语境(computational context)。这个语境可能是这个computation可能带有值,或是有可能会失败(像
Maybe
跟Either a
),或是他可能有多个值(像lists),等等。
所以,别叫盒子了,叫计算语境,fmap相当于对这个计算语境追加一层转换(做映射)
Lifting
再看一遍fmap
的类型定义:
fmap :: Functor f => (a -> b) -> f a -> f b
输入一个map a to b的函数和一个Functor实例a
,返回另一个Functor
实例b
,没什么特别的
换个姿势再看:
fmap :: Functor f => (a -> b) -> (f a -> f b)
输入一个map a to b的函数,返回另一个函数,这个函数的作用也是map a to b,但处于Functor
的语境里(参数和返回值都被包进了Functor
里),好像有那么点意思了
把一个函数转换为另一个环境下的对应函数,称为lifting(提升?没发现合适的翻译):
Lifting is a concept which allows you to transform a function into a corresponding function within another (usually more general) setting.
看这个例子:
> replicate 3 'a'
"aaa"
> :t replicate
replicate :: Int -> a -> [a]
> :t liftA2 replicate
liftA2 replicate :: (Applicative f) => f Int -> f a -> f [a]
> (liftA2 replicate) [1,2,3] ['a','b','c']
["a","b","c","aa","bb","cc","aaa","bbb","ccc"]
> :t liftA2
liftA2 :: (Applicative f) => (a -> b -> c) -> (f a -> f b -> f c)
其中liftA2
所做的事情就是lifting,根据一个普通函数(replicate
)制造一个功能类似的新函数,新的能够应用于另一个环境(Applicative
上下文):
-- 普通函数
Int -> a -> [a]
-- lift一下
f Int -> f a -> f [a]
所以,lift就是方便让普通函数能够在f
的语境里正常工作
P.S.类似的lift函数共有3个:
liftA :: Applicative f => (a -> b) -> f a -> f b
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d
更多参数的可以通过<$>
和<*>
秒秒钟定义出来,见下面Applicative instances小节的(->) r
部分
二.Functor laws
之前有提到:
实现Functor时需要遵循一些规则,比如不希望List元素顺序发生变化,希望二叉搜索树仍保留其结构性质等等
所以functor laws的作用就是约束fmap
,让映射结果保持一些性质:
如果遵守了functor laws,我们知道对它做
fmap
不会做多余的事情,只是用一个函数做映射而已
一共2条规则:
fmap id = id
fmap (f . g) = fmap f . fmap g
P.S.第二条也可以写作fmap (f . g) F = fmap f (fmap g F)
,去掉组合更容易理解一些
第一条,如果我们对functor做map id,那么得到的新functor应该与原来的完全一样
第二条,将两个函数组合起来并将结果map over一个functor的结果,应该跟先将第一个函数map over一个functor,再将第二个函数map over第一步得到的functor的结果完全一样
(内置的)Functor类实例都满足这两条规则,例如:
> fmap id (Just 3)
Just 3
> fmap id Nothing
Nothing
> fmap ((+1) . (*2)) (Just 3)
Just 7
> fmap (+1) . fmap (*2) $ Just 3
Just 7
但手动实现的Functor实例就不一定了,因为这两条规则只是道德约束,没有强检查,所以在实现自定义Functor实例时应该注意自觉遵守
三.Applicative functors
看名字叫加强版的Functor,那么强在哪里?
我们知道Functor圈定了一类能被map over的东西,可以对着Functor实例用fmap
,把普通函数作用于Functor的计算语境
似乎足够强大了,但有些特殊场景,例如:
> :t fmap (+) (Just 3)
fmap (+) (Just 3) :: Num a => Maybe (a -> a)
这是个什么东西?Maybe里装了个函数(即Just (+3)
),那这个函数要怎么拿出来用呢?
比如想作用于Just 2
的话,我们这样做:
> let (Just f) = (Just (+3)) in fmap f (Just 2)
Just 5
先模式匹配取出(+3)
,再对Just 2
做(+3)
映射,因为我们无法单纯用fmap
把包在一个Functor里的函数作用于另一个包在Functor里的值上
那么有没有一种对任何Functor都有效的通用模式,能帮助我们完成这个事情(把一个Functor里的函数作用于另一个Functor里的值)?
有。这个东西就是Applicative
:
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
(摘自Applicative)
要求必须先是Functor
实例,所以Applicative
是一种特殊的Functor
,所以也被称为Applicative functors
定义了两个接口pure
与<*>
pure
把一个普通值放到一个缺省的context下,一个最小的context但仍然包含这个值
怎么理解?看示例:
> pure 1 :: [Int]
[1]
> pure 1 :: Maybe Int
Just 1
> pure 1 :: IO Int
1
对于List而言,最小的context就是[]
(空List),所以把1
放进来得到[1]
。Maybe
的话,Nothing
没有储值的能力,context
只能是Just
,所以是Just 1
。I/O Action的话,当然是return 1
(通过return
把值放进I/O Action里)
<*>
的作用是:
It applies the wrapped function to the wrapped value
这正是我们想要的,把一个Functor里的函数作用于另一个Functor里的值
所以,Applicative
对Functor
的增强体现在<*>
函数上,增强方式是让这些Functor
实例都实现个<*>
,支持把一个Functor里的函数作用于另一个Functor里的值
带来2个好处,其一是对多参函数更友好:
如果只是普通的functor的话,我们只能将一个(单参)函数map over这个functor。但有了applicative functor,我们可以对好多个functor套用一个(多参)函数
其二是允许Functor结合(而不像fmap
算一次得到个Functor就只能结束了,通过<*>
能够继续运算下去):
applicative functor不只是有趣而且实用,它允许我们结合不同种类的计算,像是I/O计算,non-deterministic的计算,有可能失败的计算等等。而使用
<$>
跟<*>
我们可以将普通的函数来运作在任意数量的applicative functors上。
例如:
> (+) <$> (Just 1) <*> (Just 2)
Just 3
> (\a b c -> a + b + c) <$> (Just 1) <*> (Just 2) <*> (Just 3)
Just 6
> (+3) <$> ((+) <$> (Just 1) <*> (Just 2))
Just 6
四.Applicative instances
Applicative类有很多实例:
instance Monoid m => Applicative (Const m)
-- Defined in ‘Data.Functor.Const’
instance Applicative (Either e) -- Defined in ‘Data.Either’
instance Applicative ZipList -- Defined in ‘Control.Applicative’
instance Monad m => Applicative (WrappedMonad m)
-- Defined in ‘Control.Applicative’
instance Control.Arrow.Arrow a => Applicative (WrappedArrow a b)
-- Defined in ‘Control.Applicative’
instance Applicative [] -- Defined in ‘GHC.Base’
instance Applicative Maybe -- Defined in ‘GHC.Base’
instance Applicative IO -- Defined in ‘GHC.Base’
instance Applicative ((->) a) -- Defined in ‘GHC.Base’
instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’
Maybe
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just f) <*> something = fmap f something
对Maybe
类型而言,最小的能让值参与运算的context就是Just something,从Nothing
中取不出函数,所以结果一定是Nothing
,如果左侧不是Nothing
,就模式匹配从中取出函数f
,并通过fmap
作用于右侧的Maybe
实例(something
)
List
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]
pure f
就是[f]
,而[f] <*> xs
将左边的每个函数套用至右边的每个值
P.S.很容易发现pure f <*> xs
其实等价于fmap f xs
,这也是Applicative laws其中一条
IO
instance Applicative IO where
pure = return
a <*> b = do
f <- a
x <- b
return (f x)
pure
的对应实现就是return
,把一个值包进I/O Action,让它能够参与IO
运算,<*>
所作的事情就是分别从左右两侧的I/O Action里取出函数和值,做完运算再用return
包好结果
(->) r
instance Applicative ((->) r) where
pure x = (\_ -> x)
f <*> g = \x -> f x (g x)
这个看起来有些奇怪,pure
生成一个返回常量的函数,<*>
把左右两侧的函数组合起来
例如:
(+) <$> (+3) <*> (*100)
其中<$>
就是中缀版的fmap
,如下:
infixl 4 <$>
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
<*>
与<$>
都是infixl 4
(中缀左结合,优先级为4
),所以展开过程是这样:
(+) <$> (+3) <*> (*100)
=(fmap (+) (+3)) <*> (*100)
=((.) (+) (+3)) <*> (*100)
=((+) . (+3)) <*> (*100)
=\x -> ((+) . (+3)) x ((*100) x)
=\x -> (+) ((+3) x) ((*100) x)
即:
f1 <$> f2 <*> f3
=\x -> f1 (f2 x) (f3 x)
将两个applicative functor喂给
<*>
可以产生一个新的applicative functor,所以如果我们丢给他两个函数,我们能得到一个新的函数
所以f1 <$> f2 <*> f3
的实际效果是:制造一个把f2
和f3
的结果作为参数调用f1
的函数。那么就有:
f1 <$> f2 <*> f3 <*> ... <*> fn
=\x -> f1 (f2 x) (f3 x) ... (fn x)
P.S.f1 <$> f2 <*> f3
这种固定模式有个工具函数,叫liftA2
:
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f <$> a <*> b
liftA2
接受一个普通的二元函数,并将他升级成一个函数可以运作在两个functor之上
这就是所谓的lifting(升级?)
ZipList
对比List:
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]
ZipList
的实现如下:
instance Applicative ZipList where
pure x = ZipList (repeat x)
ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs)
P.S.ZipList
位于Control.Applicative
模块,之所以存在ZipList,是因为无法再赋予List另一种不同的Applicative
实现
pure
实际上生成了一个无限长的ZipList,这是因为zipWith
结果以两个List中较短的那个为准,所以,为了保证x
能正常参与运算(满足另一侧任意长度的List),所以对于ZipList而言,ZipList (repeat x)
就是最小的那个context
<*>
是从左侧取出函数List,从右侧取出数据List,再对两个List的元素一一结对做映射(zipWith
)
让左侧函数List里只有同一个函数的话,就相当于拿这个函数对右侧List做映射:
> getZipList $ pure (+1) <*> (ZipList [1, 2, 3])
[2,3,4]
P.S.其中getZipList :: ZipList a -> [a]
是个getter(具体见类型_Haskell笔记3 | Record),用来取出里面的List
换个花样:
> getZipList $ (+) <$> ZipList [1, 2, 3] <*> (ZipList [1, 2, 3])
[2,4,6]
> getZipList $ max <$> ZipList [1, 3, 4, 5] <*> ZipList [2, 0]
[2,3]
五.Applicative laws
同样,Applicative
也要遵循一些规则:
pure f <*> x = fmap f x
pure id <*> v = v
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
pure f <*> pure x = pure (f x)
u <*> pure y = pure ($ y) <*> u
通过pure
让普通函数f
能够参与Functor运算,所以有:
pure f <*> x = fmap f x
pure id <*> v = v
pure f <*> pure x = pure (f x)
通过<*>
让左侧Functor中的函数能够作用于右侧Functor中的值,所以:
-- $固定参数位置
u <*> pure y = pure ($ y) <*> u
-- (.)改变结合性
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
内置的Applicative
实例都遵从这些规则,但同样只是道德约束,手动实现Applicative
实例时要自觉遵守
Applicative style
通过<$>
和<*>
可以达到非常优雅的调用式风格。例如:
> (++) <$> Just "johntra" <*> Just "volta"
Just "johntravolta"
类比函数调用:
> (++) "johntra" "volta"
"johntravolta"
在I/O场景更明显:
myAction = do
a <- getLine
b <- getLine
return $ a ++ b
对应的applicative style:
myAction = (++) <$> getLine <*> getLine
相当优雅,让Functor层面的运算与普通运算在形式上几乎没什么差异了(从形式上消除了运算所处context的差异)