您的位置:首页 > 移动开发

Functor与Applicative_Haskell笔记7

2021-01-14 20:46 916 查看

一.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 and box

函数也是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元素顺序发生变化,希望二叉搜索树仍保留其结构性质等等

(摘自深入typeclass_Haskell笔记4)

所以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]是因为ZipList没法show(没派生自Show类),所以取出里面的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的差异)

参考资料
Lifting

What is “lifting” in Haskell?

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: