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

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

虽然Swift并不是一个函数式语言,但是我们可以用更多点代码来完成与Haskell同样的结果。Swift的代码见on GitHub.

这里是一个简单的值:

如果我们应用一个函数(+3)到这个值:

非常简答,是不是?现在我们拓展一下,上面的值我们没有设定上下文场景,如果我们假设一个值处于一种上下文场景context中,你可以将上下文场景看成是一个盒子,盒子里面放入的是一个值:

为什么要假设上下文场景呢?其实任何事物都无法脱离其与环境的关系,任何真理都是有上下文前提的,当然除了这条。

当值放入一个盒子上下文中时,如果你想将一个函数应用到这个值时,取决于上下文场景你会得到不同的结果。这个思想其实是Functors, Applicatives, Monads, Arrows等概念的基础。

那么上下文使用什么语法表达呢?再Haskell中使用Maybe,在Swift中使用Optional:

 

Swift:

enum Optional<T> {

case None

case Some(T)

}

Haskell:

data Maybe a = Nothing | Just a

Optional和Maybe类似两个盒子,其中包裹着两个不同的值函数:无(None)和有(Some)。

下面我们看看上面Swift中两个函数 .Some(T).None.不同,也就是Haskell两个函数Nothing与Just a的不同。现在来谈谈函子。

Functor函子

当一个值被一个上下文包裹时,你不能使用普通函数应用到这个值:

这就是需要 map 的用途 (fmap in Haskell), map 知道如何将普通函数应用到一个被上下文包裹的值中,比如,假设你要应用一个函数:+3,将这个函数应用到 .Some(2). 使用 map:

func plusThree(addend: Int) -> Int {

return addend + 3

}

Optional.Some(2).map(plusThree)

// => .Some(5)

Haskell使用fmap,将+3函数应用到Just 2:

> fmap (+3) (Just 2)

Just 5

fmap告诉我们它已经成功完成,但是它是如何应用函数到盒子里的值呢?

函子到底是什么?

一个函子Functor是任意类型,这些类型定义了如何应用 map (fmap in Haskell) 。 也就是说,如果我们要将普通函数应用到一个有盒子上下文包裹的值,那么我们首先需要定义一个叫Functor的数据类型,在这个数据类型中需要定义如何使用map或fmap来应用这个普通函数。这个函子Functor如下图:

fmap的输入参数是a->b函数,在我们这个案例中是(+3),然后定义一个函子Functor,这里是Haskell的Just 2,最后返回一个新的函子,在我们案例中,使用Haskell是Just 5。

我们再看看在Swift中如何实现,下面是autoclosure实现的函子:

Optional.Some(2).map { $0 + 3 }

// => .Some(5)

这里 map 魔术地使用了我们的函数(+3),这是因为 Optional 是一个函子,它定义了map如何将外部指定的普通函数应用到被上下文包裹的值函数 Somes 和 Nones中:

func map<U>(f: T -> U) -> U? {

swifth self {

case .Some(let x): return f(x)

case .None: return .None

}

下图展示了函子内部工作原理:

第一步是将值从上下文盒子中解救出来,然后将外部指定的函数(+3)应用到这个值上,得到一个新的值(5),再将这个新值放入到上下文盒子中。是不是很形象生动?

当然,你也可以将(+3)应用到None上:

Optional.None.map { $0 + 3 }

// => .None

map或fmap知道如果你应用函数到None,你同样得到的还是无None,这里map或fmap是不是像禅一样呢?

现在我们知道Optional类型或Maybe类型存在的原因了吧?我们再用实际中案例说明,如果我们使用一种没有Optional/Maybe类型的语言从数据库查询结果,比如Ruby:

let post = Post.findByID(1)

if post != nil {

return post.title

} else {

return nil

}

而使用有Optional/Maybe类型的语言Swift/Haskell,则可以使用函子Optional:

findPost(1).map(getPostTitle)

如果 findPost(1) 返回一个帖子, 我们会使用 getPostTitle函数得到这个帖子的标题,如果返回无 None, 我们也返回无None!

我们可以甚至定义一个中缀操作符号,Swift的map的中缀操作符是<^>,而Haskell的fmap是<$>。:

Haskell直接写为:

getPostTitle <$> (findPost 1)

Swift代码如下:

infix operator <^> { associativity left }

func <^><T, U>(f: T -> U, a: T?) -> U? {

return a.map(f)

}

getPostTitle <^> findPost(1)

 

下图显示了如何将一个普通函数应用到值集合,不是单个值,而是值的集合数组中:

图中数组函子将数组一个个打开(遍历),然后分别将普通函数应用到这些元素中,最后返回一个新的集合值。Haskell中定义:

instance Functor [] where fmap = map

下面我们看看将一个函数应用到另外一个函数的情况:

fmap (+3) (+1)

这里我们将函数(+3)应用到(+1)函数上,首先我们看看什么是函数,见下图:

一个函数是输入一个值,返回一个值。

下图是将一个函数应用到另外一个函数:

结果就是另外一个函数!

> import Control.Applicative

> let foo = fmap (+3) (+2)

> foo 10

15

所以说,函数也是一个函子functor:

instance Functor ((->) r) where fmap f g = f . g

你在一个函数上使用fmap ,实际你在做函数的组合,如同堆积木一样。

Swift将一个函数应用到另外一个函数的代码如下:

typealias IntFunction = Int -> Int

func map(f: IntFunction, _ g: IntFunction) -> IntFunction {

return { x in f(g(x)) }

}

let foo = map({ $0 + 2 }, { $0 + 3 })

foo(10)

// => 15

Applicative

当我们的值被一个上下文包裹,就像函子Functor:

之前我们讨论的是如何将一个普通函数应用到这个函子中,现在如果这个普通函数也是一个被上下文包裹的,怎么办?

在Haskell中,Control.Applicative 定义了 <*>, 它能知道如何应用一个被上下文包裹的函数到一个被上下文包裹的值中。

上图可见,Applicative内部也是将各自包裹的盒子打开,应用其中函数与值的计算,然后包裹新值在一个上下文中。

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

Swift并没有内建的Applicative. 可以显式打开包裹遍历实现::

extension Optional {

func apply<U>(f: (T -> U)?) -> U? {

switch f {

case .Some(let someF): return self.map(someF)

case .None: return .None

}

}

}

extension Array {

func apply<U>(fs: [Element -> U]) -> [U] {

var result = [U]()

for f in fs {

for element in self.map(f) {

result.append(element)

}

}

return result

}

}

如果 self 和函数都是 .Some, 那么函数应用到解开包裹的optionm,否则返回 .None.

也可以使用定义 <*>来做同样事情:

infix operator <*> { associativity left }

func <*><T, U>(f: (T -> U)?, a: T?) -> U? {

return a.apply(f)

}

func <*><T, U>(f: [T -> U], a: [T]) -> [U] {

return a.apply(f)

}

i.e:

Optional.Some({ $0 + 3 }) <*> Optional.Some(2)

// => 5

使用 <*> 会有趣的事情发生:

> [(*2), (+3)] <*> [1, 2, 3]

[2, 4, 6, 4, 5, 6]

下面是使用Applicative能实现,而使用函子Functor不能实现的,你如何应用一个带有两个输入参数的函数到两个已经包裹的值中?

使用函子的代码如下:

> (+) <$> (Just 5)

Just (+5)

> Just (+5) <$> (Just 4)

ERROR ??? WHAT DOES THIS EVEN MEAN WHY IS THE FUNCTION WRAPPED IN A JUST

第二个发生错误了。

使用Applicative:

> (+) <$> (Just 5)

Just (+5)

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

Just 8

Swift的代码如下:

func curriedAddition(a: Int)(b: Int) -> Int {

return a + b

}

curriedAddition <^> Optional(2) <^> Optional(3)

// => COMPILER ERROR: Value of optional type '(Int -> Int)? not unwrapped; did you mean to use '!' or '??'

Applicative:

curriedAddition <^> Optional(2) <*> Optional(3)

ApplicativeFunctor 推到到一边. “只有大孩子才能使用带有任何数量参数的函数。”

func curriedTimes(a: Int)(b: Int) -> Int {

return a * b

}

curriedTimes <^> Optional(5) <*> Optional(3)

Monad

函子funtor是将一个普通函数应用到包裹的值:

Applicative应用一个包裹的函数到一个包裹的值:

Monad 则是将一个会返回包裹值的函数应用到一个被包裹的值上,Haskell中使用“>>=”表示,而Swift使用“|”.

Haskell中的MayBe也是一个monad:

假设half是一个只工作于偶数数字的函数:

half x = if even x 

then Just (x `div` 2)

else Nothing

Swift代码如下:

func half(a: Int) -> Int? {

return a % 2 == 0 ? a / 2 : .None

}

对于这个half函数,其形象工作原理入下图,如果输入一个值,那么half会返回一个被包裹的值。

但是如果我们输入一个包裹的值,而不是普通的值呢?

这时原来的代码就不工作了,我们需要使用新的语法Haskell是“>>=”来将包裹后的值放入函数,“>>=”类似我们排堵的拔子:

代码工作如下:

> Just 3 >>= half

Nothing

> Just 4 >>= half

Just 2

> Nothing >>= half

Nothing

Swift的代码:

Optional(3) >>- half

// .None

Optional(4) >>- half

// 2

Optional.None >>- half

// .None

那么内部发生了什么?Monad 是另外一个typeclass. 这里是partial定义:

class Monad m where

(>>=) :: m a -> (a -> m b) -> m b

Swift代码:

// For Optional

func >>-<T, U>(a: T?, f: T -> U?) -> U?

// For Array

func >>-<T, U>(a: [T], f: T -> [U]) -> [U]

monad工作原理图如下:

首先获得一个Monad,如Just 3,其次定义一个返回Monad的函数如half,最后结果也会返回一个Monad。

Haskell中Maybe也是一个Monad:

instance Monad Maybe where

Nothing >>= func = Nothing

Just val >>= func = func val

Swift的Optional也是一个Monad。

下面是输入一个包裹值到一个函数中完整示意图:

第一步,绑定已经解除包裹的值,第二步,将已经解除包裹的值输入函数,第三步,一个被重新包裹的值被输出。

如果你输入无None,更加简单:

你可以像链条一样链接这些调用:

> Just 20 >>= half >>= half >>= half

Nothing

Swift代码:

Optional(20) >>- half >>- half >>- half

// => .None

总结

  1. 函子functor是一种实现fmap或map的数据类型
  2. applicative是一种实现了Applicative 或apply的数据类型
  3. monad是一种实现了Monad或flatmap的数据类型.
  4. Haskell的Maybe和Swift的Optional是functor函子 applicative和Monad。.

那么函子、applicative和Monad三个区别是什么?

  • functor: 应用一个函数到包裹的值,使用fmap/map.
  • applicative: 应用一个包裹的函数到包裹的值。
  • monad: 应用一个返回包裹值的函数到一个包裹的值。

本文另外一篇中文翻译:Functor, Applicative, 以及 Monad 的图片阐释

函数编程之道

什么是Monad

函数式编程

http://www.jdon.com/idea/functor-monad.html

函数编程中functor和monad的形象解释的更多相关文章

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

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

  2. 转OSGchina中,array老大的名词解释

    转OSGchina中,array老大的名词解释 转自:http://ydwcowboy.blog.163.com/blog/static/25849015200983518395/ osg:: Cle ...

  3. 嵌入式中的 *(volatile unsigned int *)0x500 解释

    C语言中*(volatile unsigned int *)0x500的解释: 如下: (unsigned int *)0x500:将地址0x500强制转化为int型指针*(unsigned int ...

  4. VC++中几种字符标志的解释

    VC++中几种字符标志的解释 LPSTR = char * LPCSTR = const char * LPWSTR = wchar_t * LPCWSTR = const wchar_t * LPO ...

  5. 31 Python中 sys.argv[]的用法简明解释(转)

    Python中 sys.argv[]的用法简明解释 因为是看书自学的python,开始后不久就遇到了这个引入的模块函数,且一直在IDLE上编辑了后运行,试图从结果发现它的用途,然而结果一直都是没结果, ...

  6. Spring中IOC和AOP的详细解释(转)

    原文链接:Spring中IOC和AOP的详细解释 我们是在使用Spring框架的过程中,其实就是为了使用IOC,依赖注入,和AOP,面向切面编程,这两个是Spring的灵魂. 主要用到的设计模式有工厂 ...

  7. [js]js的惰性声明, js中声明过的变量(预解释),后在不会重新声明了

    js的惰性声明, js中声明过的变量(预解释),后在不会重新声明了 fn(); // 声明+定义 js中声明过一次的变量,之后在不会重新声明了 function fn() { console.log( ...

  8. Functor and Monad in Swift

    I have been trying to teach myself Functional Programming since late 2013. Many of the concepts are ...

  9. Python中 sys.argv的用法简明解释

    Python中 sys.argv[]的用法简明解释 sys.argv[]说白了就是一个从程序外部获取参数的桥梁,这个“外部”很关键,所以那些试图从代码来说明它作用的解释一直没看明白.因为我们从外部取得 ...

随机推荐

  1. linux基础随记

    1.cd 切换路径 cd ~vbird 进入这个用户的主目录cd ~ 进入root这个目录下cd .. 进入root上层目录cd - 进入root这个目录下cd /var/spool/mail 直接访 ...

  2. 爬虫系列(十三) 用selenium爬取京东商品

    这篇文章,我们将通过 selenium 模拟用户使用浏览器的行为,爬取京东商品信息,还是先放上最终的效果图: 1.网页分析 (1)初步分析 原本博主打算写一个能够爬取所有商品信息的爬虫,可是在分析过程 ...

  3. 50.常用的query查询方式

    主要知识点 match all match multi match range query term query terms query exist query         1.match all ...

  4. Spring Cloud 各个组件介绍

    从上图可以看出 Spring Cloud 各个组件相互配合,合作支持了一套完整的微服务架构: Eureka 负责服务的注册与发现,很好地将各服务连接起来. Hystrix 负责监控服务之间的调用情况, ...

  5. Javascript 基础夯实 —— 使用 webWorker 实现多线程(转)

    原文链接:https://zhuanlan.zhihu.com/p/29219879 当我们开始学习 javascript 的时候,我们就知道 js 其实是单线程的,所以当我们在浏览器中运行某些耗时算 ...

  6. 检測wifi是否须要portal验证 公共场所wifi验证

    何为wifi portal验证? 平时在商场,咖啡厅,银行等公共场所.我们手机提示:有可用WLAN.这些WIFI能够直接连接,不须要password,但须要我们手动在手机网页上进行验证,通常是输入一个 ...

  7. IA32 MMU paging初始化代码

    写了一段IA32 paging通用构造代码.有须要的.能够拿去 #define PDE_FLG_RW (1<<1) #define PDE_FLG_US (1<<2) #def ...

  8. swing Jlable中存放变量显示问题

    java swing 学习 在做一个ATM机系统小案例中.碰到JLable中存放变量,变量发生改变.而JLable中还是显示原来的值,网上寻找答案,用updateUI()和revalidate();方 ...

  9. MySQL 时间类型字段的分析

    日期类型                存储空间               日期格式                                           日期范围---------- ...

  10. cocos2d-x 3.0游戏实例学习笔记《卡牌塔防》第0步---知识点总结&amp;效果预览&amp;设计思路

    /* 说明: **1.本次游戏实例是<cocos2d-x游戏开发之旅>上的最后一个游戏.这里用3.0重写并做下笔记 **2.我也问过木头本人啦,他说:随便写.第一别全然照搬代码:第二能够说 ...