FunctorApplicativeMonad 是函数式编程语言中三个非常重要的概念,尤其是 Monad

说明:本文中的主要代码为 Haskell 语言,它是一门纯函数式的编程语言。

一、结论

关于 Functor、Applicative 和 Monad 的概念,其实各用一句话就可以概括:

  • 一个 Functor 就是一种实现了 Functor typeclass 的数据类型;
  • 一个 Applicative 就是一种实现了 Applicative typeclass 的数据类型;
  • 一个 Monad 就是一种实现了 Monad typeclass 的数据类型。

typeclass

A typeclass is a sort of interface that defines some behavior. If a type is a part of a typeclass, that means that it supports and implements the behavior the typeclass describes. A lot of people coming from OOP get confused by typeclasses because they think they are like classes in object oriented languages. Well, they’re not. You can think of them kind of as Java interfaces, only better.

typeclass 类似于 Java 中的接口,或者 Objective-C 中的协议。在 typeclass 中定义了一些函数,实现一个 typeclass 就是要实现这些函数,而所有实现了这个 typeclass 的数据类型都会拥有这些共同的行为。

Applicative 是增强型的 Functor ,一种数据类型要成为 Applicative 的前提条件是它必须是 Functor ;同样的,Monad 是增强型的 Applicative ,一种数据类型要成为 Monad 的前提条件是它必须是 Applicative 。

二、Maybe

在正式开始介绍 Functor、Applicative 和 Monad 的定义前,我想先介绍一种非常有意思的数据类型,Maybe 类型(可类比 Swift 中的 Optional):

The Maybe type encapsulates an optional value. A value of type Maybe a either contains a value of type a (represented as Just a), or it is empty (represented as Nothing). Using Maybe is a good way to deal with errors or exceptional cases without resorting to drastic measures such as error.

Maybe 类型封装了一个可选值。一个 Maybe a 类型的值要么包含一个 a 类型的值(用 Just a 表示);要么为空(用 Nothing 表示)。

我们可以把 Maybe 看作一个盒子,这个盒子里面可能装着一个 a 类型的值,即 Just a ;也可能是一个空盒子,即 Nothing 。或者,你也可以把它理解成泛型,比如 Objective-C 中的 NSArray<ObjectType>。不过,最正确的理解应该是把 Maybe 看作一个上下文,这个上下文表示某次计算可能成功也可能失败,成功时用 Just a 表示,a 为计算结果;失败时用 Nothing 表示,这就是 Maybe 类型存在的意义:

data Maybe a = Nothing | Just a

下面,我们来直观地感受一下 Maybe 类型:

ghci> Nothing
Nothing
ghci> Just 2
Just 2

我们可以用盒子模型来理解一下,Nothing 就是一个空盒子;而 Just 2 则是一个装着 2 这个值的盒子:

Maybe 类型实现了 Functor typeclass、Applicative typeclass 和 Monad typeclass ,所以它同时是 Functor、Applicative 和 Monad ,具体实现细节将在下面的章节进行介绍。

三、Functor

在正式开始介绍 Functor 前,先思考一个这样的问题,假如有一个值 2 :

我们如何将函数 (+3) 应用到这个值上呢?

ghci> (+3) 2

那么问题来了,如果这个值 2 是在一个上下文中呢?比如 Maybe ,此时,这个值 2 就变成了 Just 2 :

这个时候,我们就不能直接将函数 (+3) 应用到 Just 2 了。那么,我们如何将一个函数应用到一个在上下文中的值呢?

Functor 就是干这事的。

四、Functor typeclass

首先,我们来看一下 Functor typeclass 的定义:

class Functor f where
fmap :: (a -> b) -> f a -> f b

Functor typeclass 中定义了一个函数 fmap,它将一个函数 (a -> b) 应用到一个在上下文中的值 f a,并返回另一个在相同上下文中的值 f b,这里的 f 是一个类型占位符,表示任意类型的 Functor 。

注:fmap 函数可类比 Swift 中的 map 方法。

五、Maybe Functor

我们知道 Maybe 类型就是一个 Functor ,它实现了 Functor typeclass 。我们将类型占位符 f 用具体类型 Maybe 代入可得:

class Functor Maybe where
fmap :: (a -> b) -> Maybe a -> Maybe b

因此,对于 Maybe 类型来说,它要实现的函数 fmap 的功能就是将一个函数 (a -> b) 应用到一个在 Maybe 上下文中的值 Maybe a,并返回另一个在 Maybe 上下文中的值 Maybe b。接下来,我们一起来看一下 Maybe 类型实现 Functor typeclass 的具体细节:

instance Functor Maybe where
fmap func (Just x) = Just (func x)
fmap func Nothing = Nothing

这里针对 Maybe 上下文的两种情况分别进行了处理:如果盒子中有值,即 Just x,那么就将 x 从盒子中取出,然后将函数 func 应用到 x ,最后将结果放入一个相同类型的新盒子中;如果盒子为空,那么直接返回一个新的空盒子。

看到这里,我想你应该已经知道如何将一个函数应用到一个在上下文中的值了。比如前面提到的将函数 (+3) 应用到 Just 2

ghci> fmap (+3) (Just 2)
Just 5

另外,值得一提的是,当我们将函数 (+3) 应用到一个空盒子,即 Nothing 时,我们将会得到一个新的空盒子:

ghci> fmap (+3) Nothing
Nothing

六、Applicative

现在,我们已经知道如何将函数 (+3) 应用到 Just 2 了。那么问题又来了,如果函数 (+3) 也在上下文中呢,比如 Maybe ,此时,函数 (+3) 就变成了 Just (+3)

那么,我们如何将一个在上下文中的函数应用到一个在上下文中的值呢?

这就是 Applicative 要干的事,详情请看下节内容。

七、Applicative typeclass

同样的,我们先来看一下 Applicative typeclass 的定义:

class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

我们注意到,与 Functor typeclass 的定义不同的是,在 Applicative typeclass 的定义中多了一个类约束 Functor f,表示的意思是数据类型 f 要实现 Applicative typeclass 的前提条件是它必须要实现 Functor typeclass,也就是说它必须是一个 Functor。

在 Applicative typeclass 中定义了两个函数:

  • pure:将一个值 a 放入上下文中;
  • (<*>):将一个在上下文中的函数 f (a -> b) 应用到一个在上下文中的值 f a,并返回另一个在上下文中的值 f b

八、Maybe Applicative

同样的,我们将类型占位符 f 用具体类型 Maybe 代入,可得:

class Functor Maybe => Applicative Maybe where
pure :: a -> Maybe a
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b

因此,对于 Maybe 类型来说,它要实现的 pure 函数的功能就是将一个值 a 放入 Maybe 上下文中。而 (<*>) 函数的功能则是将一个在 Maybe 上下文中的函数 Maybe (a -> b) 应用到一个在 Maybe 上下文中的值 Maybe a,并返回另一个在 Maybe 上下文中的值 Maybe b。接下来,我们一起来看一下 Maybe 类型实现 Applicative typeclass 的具体细节:

instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just func) <*> something = fmap func something

pure 函数的实现非常简单,直接等于 Just 即可。而对于 (<*>) 函数的实现,我们同样需要针对 Maybe 上下文的两种情况分别进行处理:当装函数的盒子为空时,直接返回一个新的空盒子;当装函数的盒子不为空时,即 Just func,则取出 func ,使用 fmap 函数直接将 func 应用到那个在上下文中的值,这个正是我们前面说的 Functor 的功能。

好了,我们接下来看一下将 Just (+3) 应用到 Just 2 的具体过程:

ghci> Just (+3) <*> Just 2
Just 5

同样的,当我们将一个空盒子,即 Nothing 应用到 Just 2 的时候,我们将得到一个新的空盒子:

ghci> Nothing <*> Just 2
Nothing

九、Monad

截至目前,我们已经知道了 Functor 的作用就是应用一个函数到一个上下文中的值:

而 Applicative 的作用则是应用一个上下文中的函数到一个上下文中的值:

那么 Monad 又会是什么呢?

其实,Monad 的作用跟 Functor 类似,也是应用一个函数到一个上下文中的值。不同之处在于,Functor 应用的是一个接收一个普通值并且返回一个普通值的函数,而 Monad 应用的是一个接收一个普通值但是返回一个在上下文中的值的函数:

十、Monad typeclass

同样的,我们先来看一下 Monad typeclass 的定义:

class Applicative m => Monad m where
return :: a -> m a (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y fail :: String -> m a
fail msg = error msg

在 Monad typeclass 中定义了四个函数,分别是 return(>>=)(>>)fail,且后两个函数 (>>) 和 fail 给出了默认实现,而在绝大多数情况下,我们都不需要去重写它们。因此,去掉这两个函数后,Monad typeclass 的定义可简化为:

class Applicative m => Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b

跟 Applicative typeclass 的定义一样,在 Monad typeclass 的定义中也有一个类约束 Applicative m,表示的意思是一种数据类型 m 要成为 Monad 的前提条件是它必须是 Applicative。

另外,其实 return 函数的功能与 Applicative 中的 pure 函数的功能是一样的,只不过换了一个名字而已,它们的作用都是将一个值 a 放入上下文中。而 (>>=) 函数的功能则是应用一个(接收一个普通值 a 但是返回一个在上下文中的值 m b 的)函数 (a -> m b) 到一个上下文中的值 m a ,并返回另一个在相同上下文中的值 m b 。

注:>>= 函数的发音为 bind。另外,>>= 函数可类比 Swift 中的 flatMap 方法。

十一、Maybe Monad

同样的,我们将类型占位符 m 用具体类型 Maybe 代入,可得:

class Applicative Maybe => Monad Maybe where
return :: a -> Maybe a
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b

相信你用盒子模型已经能够轻松地理解上面两个函数了,因此不再赘述。接下来,我们一起来看一下 Maybe 类型实现 Monad typeclass 的具体细节:

instance Monad Maybe where
return x = Just x
Nothing >>= func = Nothing
Just x >>= func = func x

正如前面所说,return 函数的实现跟 pure 函数一样,直接等于 Just 函数即可,功能就是将一个值 x 放入 Maybe 盒子中,变成 Just x。同样的,对于 (>>=) 函数的实现,我们需要针对 Maybe 上下文的两种情况分别进行处理,当盒子为空时,直接返回一个新的空盒子;当盒子不为空时,即 Just x ,则取出 x ,直接将 func 函数应用到 x ,而我们知道 func x 的结果就是一个在上下文中的值。

下面,我们一起来看一个具体的例子。我们先定义一个 half 函数,这个函数接收一个数字 x 作为参数,如果 x 是偶数,则将 x 除以 2 ,并将结果放入 Maybe 盒子中;如果 x 不是偶数,则返回一个空盒子:

half x = if even x
then Just (x `div` 2)
else Nothing

接下来,我们使用 (>>=) 函数将 half 函数应用到 Just 20,假设得到结果 y ;然后继续使用 (>>=) 函数将 half 函数应用到上一步的结果 y,以此类推,看看会得到什么样的结果:

ghci> Just 20 >>= half
Just 10
ghci> Just 10 >>= half
Just 5
ghci> Just 5 >>= half
Nothing

看到上面的运算过程,不知道你有没有看出点什么端倪呢?上一步的输出作为下一步的输入,并且只要你愿意的话,这个过程可以无限地进行下去。我想你可能已经想到了,是的,就是链式操作。所有的操作链接起来就像是一条生产线,每一步的操作都是对输入进行加工,然后产生输出,整个操作过程可以看作是对最初的原材料 Just 20 进行加工并最终生产出成品 Nothing 的过程:

ghci> Just 20 >>= half >>= half >>= half
Nothing

注:链式操作只是 Monad 为我们带来的主要好处之一;另一个本文并未涉及到的主要好处是,Monad 可以为我们自动处理上下文,而我们只需要关心真正的值就可以了。

十二、ReactiveCocoa

现在,我们已经知道 Monad 是什么了,它就是一种实现了 Monad typeclass 的数据类型。那么它有什么具体的应用呢?ReactiveCocoa,它就是根据 Monad 的概念搭建起来的。下面是 RACStream 的继承结构图:

RACStream 是 ReactiveCocoa 中最核心的类,它就是一个 Monad :

/// An abstract class representing any stream of values.
///
/// This class represents a monad, upon which many stream-based operations can
/// be built.
///
/// When subclassing RACStream, only the methods in the main @interface body need
/// to be overridden.
@interface RACStream : NSObject /// Lifts `value` into the stream monad.
///
/// Returns a stream containing only the given value.
+ (instancetype)return:(id)value; /// Lazily binds a block to the values in the receiver.
///
/// This should only be used if you need to terminate the bind early, or close
/// over some state. -flattenMap: is more appropriate for all other cases.
///
/// block - A block returning a RACStreamBindBlock. This block will be invoked
/// each time the bound stream is re-evaluated. This block must not be
/// nil or return nil.
///
/// Returns a new stream which represents the combined result of all lazy
/// applications of `block`.
- (instancetype)bind:(RACStreamBindBlock (^)(void))block; @end

我们可以看到,在 RACStream 中定义了两个看上去非常眼熟的方法:

+ (instancetype)return:(id)value;
- (instancetype)bind:(RACStreamBindBlock (^)(void))block;

其中,return: 方法的功能就是将一个值 value 放入 RACStream 上下文中;而 bind: 方法的功能则是将一个 RACStreamBindBlock 类型的 block 应用到一个在 RACStream 上下文中的值(receiver),并返回另一个在 RACStream 上下文中的值。注:RACStreamBindBlock 类型的 block 就是一个接收一个普通值 value 但是返回一个在 RACStream 上下文中的值的“函数”:

/// A block which accepts a value from a RACStream and returns a new instance
/// of the same stream class.
///
/// Setting `stop` to `YES` will cause the bind to terminate after the returned
/// value. Returning `nil` will result in immediate termination.
typedef RACStream * (^RACStreamBindBlock)(id value, BOOL *stop);

接下来,为了加深理解,我们一起来对比一下 Monad typeclass 的定义:

class Applicative m => Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b

同样的,我们将类型占位符 m 用 RACStream 代入,可得:

class Applicative RACStream => Monad RACStream where
return :: a -> RACStream a
(>>=) :: RACStream a -> (a -> RACStream b) -> RACStream b

其中,return :: a -> RACStream a 就对应 + (instancetype)return:(id)value;,而 (>>=) :: RACStream a -> (a -> RACStream b) -> RACStream b 则对应 - (instancetype)bind:(RACStreamBindBlock (^)(void))block;

注:我们前面已经提到过了,>>= 函数的发音就是 bind 。

因此,ReactiveCocoa 便有了下面的玩法:

RACSignal *signal2 = [[[signal1
bind:block1]
bind:block2]
bind:block3];

至此,我们已经知道了 ReactiveCocoa 中最核心的原理。

十三、总结

Functor、Applicative 和 Monad 是什么?

  1. 一个 Functor 就是一种实现了 Functor typeclass 的数据类型;
  2. 一个 Applicative 就是一种实现了 Applicative typeclass 的数据类型;
  3. 一个 Monad 就是一种实现了 Monad typeclass 的数据类型。

Functor、Applicative 和 Monad 三者之间的联系?

  • Applicative 是增强型的 Functor ,一种数据类型要成为 Applicative 的前提条件是它必须是 Functor ;
  • Monad 是增强型的 Applicative ,一种数据类型要成为 Monad 的前提条件是它必须是 Applicative 。

Functor、Applicative 和 Monad 三者之间的区别?

  1. Functor :使用 fmap 应用一个函数到一个上下文中的值;
  2. Applicative :使用 <*> 应用一个上下文中的函数到一个上下文中的值;
  3. Monad :使用 >>= 应用一个接收一个普通值但是返回一个在上下文中的值的函数到一个上下文中的值。

数据类型 Maybe 实现了 Functor typeclass、Applicative typeclass 和 Monad typeclass ,所以它同时是 Functor、Applicative 和 Monad 。

十四、内容来源

雷纯锋的技术博客 - Functor、Applicative 和 Monad

Functor、Applicative 和 Monad的更多相关文章

  1. 泛函编程(28)-粗俗浅解:Functor, Applicative, Monad

    经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点:我是指在现实编程的情况下所谓的泛函编程到底如何特别.我们已经习惯了传统的行令式编程(imperative progra ...

  2. Monad / Functor / Applicative 浅析

    前言 Swift 其实比 Objective-C 复杂很多,相对于出生于上世纪 80 年代的 Objective-C 来说,Swift 融入了大量新特性.这也使得我们学习掌握这门语言变得相对来说更加困 ...

  3. Functor、Applicative 和 Monad x

    首先,我们来看一下 Functor typeclass 的定义: 1 2 class Functor f where fmap :: (a -> b) -> f a -> f b F ...

  4. Functor、Applicative 和 Monad(重要)

    Functor.Applicative 和 Monad Posted by 雷纯锋Nov 8th, 2015 10:53 am Functor.Applicative 和 Monad 是函数式编程语言 ...

  5. 浅释Functor、Applicative与Monad

    引言 转入Scala一段时间以来,理解Functor.Applicative和Monad等概念,一直是我感到头疼的部分.虽然读过<Functors, Applicatives, And Mona ...

  6. 函数编程中functor和monad的形象解释

    函数编程中functor和monad的形象解释 函数编程中Functor函子与Monad是比较难理解的概念,本文使用了形象的图片方式解释了这两个概念,容易理解与学习,分别使用Haskell和Swift ...

  7. Scalaz(10)- Monad:就是一种函数式编程模式-a design pattern

    Monad typeclass不是一种类型,而是一种程序设计模式(design pattern),是泛函编程中最重要的编程概念,因而很多行内人把FP又称为Monadic Programming.这其中 ...

  8. 泛函编程(27)-泛函编程模式-Monad Transformer

    经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...

  9. 如何编写高质量的 JS 函数(3) --函数式编程[理论篇]

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/EWSqZuujHIRyx8Eb2SSidQ作者:杨昆 [编写高质量函数系列]中, <如何 ...

随机推荐

  1. java 构造器(构造方法)使用详细说明

    知识点 什么是构造器 构造器通常也叫构造方法.构造函数,构造器在每个项目中几乎无处不在.当你new一个对象时,就会调用构造器.构造器格式如下: [修饰符,比如public] 类名 (参数列表,可以没有 ...

  2. 通过filebeat、logstash、rsyslog采集nginx日志的几种方式

    由于nginx功能强大,性能突出,越来越多的web应用采用nginx作为http和反向代理的web服务器.而nginx的访问日志不管是做用户行为分析还是安全分析都是非常重要的数据源之一.如何有效便捷的 ...

  3. 并发工具类的使用 CountDownLatch,CyclicBarrier,Semaphore,Exchanger

    1.CountDownLatch 允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助. A CountDownLatch用给定的计数初始化. await方法阻塞,直到由于countDo ...

  4. javascript的“好莱坞原则”

    好莱坞原则——不要给我们打电话,我们会给你打电话(don‘t call us, we‘ll call you).在Javascript中就是:别调用我们,我们会调用你. “好莱坞原则”强调的是高层对低 ...

  5. 日常破解--从XCTF的app3题目简单了解安卓备份文件以及sqliteCipher加密数据库

    一.题目来源     题目来源:XCTF app3题目 二.解题过程     1.下载好题目,下载完后发现是.ab后缀名的文件,如下图所示:     2.什么是.ab文件?.ab后缀名的文件是Andr ...

  6. 详细解析kafka之kafka分区和副本

    本篇主要介绍kafka的分区和副本,因为这两者是有些关联的,所以就放在一起来讲了,后面顺便会给出一些对应的配置以及具体的实现代码,以供参考~ 1.kafka分区机制 分区机制是kafka实现高吞吐的秘 ...

  7. 2020Ubuntu server1804最新安装后的配置

    一.Putty进行ssh连接. 完成最基本配置之后,就远程连接服务器了.在windows是我习惯putty 在我以前的老电脑里面找一个putty是0.6版本的,连上Ip ,还是原来的配方,还是原来的味 ...

  8. Ansible-1 基本认识及清单与模块

    ansible 一.常用的自动化运维工具 1.puppet 基于ruby开发,采用c/s架构,扩展性强,基于ssl,远程命令执行相对较弱, 2.saltstack 基于python开发,采用C/S架构 ...

  9. 工具之scroolToIndex

    需求定位:导航中实现子元素滚动到父元素的最左侧 解决方案:查找该子元素的offsetLeft值,然后让父元素滚动offsetLeft,parenDom.scrollLeft = childDom.of ...

  10. 测试必知必会系列- Linux常用命令 - cd

    21篇测试必备的Linux常用命令,每天敲一篇,每次敲三遍,每月一循环,全都可记住!! https://www.cnblogs.com/poloyy/category/1672457.html 如何进 ...