函数编程中functor和monad的形象解释
函数编程中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)
Applicative 将Functor 推到到一边. “只有大孩子才能使用带有任何数量参数的函数。”
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
总结
- 函子functor是一种实现fmap或map的数据类型
- applicative是一种实现了Applicative 或apply的数据类型
- monad是一种实现了Monad或flatmap的数据类型.
- Haskell的Maybe和Swift的Optional是functor函子 applicative和Monad。.
那么函子、applicative和Monad三个区别是什么?
- functor: 应用一个函数到包裹的值,使用fmap/map.
- applicative: 应用一个包裹的函数到包裹的值。
- monad: 应用一个返回包裹值的函数到一个包裹的值。
本文另外一篇中文翻译:Functor, Applicative, 以及 Monad 的图片阐释
http://www.jdon.com/idea/functor-monad.html
函数编程中functor和monad的形象解释的更多相关文章
- 泛函编程(28)-粗俗浅解:Functor, Applicative, Monad
经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点:我是指在现实编程的情况下所谓的泛函编程到底如何特别.我们已经习惯了传统的行令式编程(imperative progra ...
- 转OSGchina中,array老大的名词解释
转OSGchina中,array老大的名词解释 转自:http://ydwcowboy.blog.163.com/blog/static/25849015200983518395/ osg:: Cle ...
- 嵌入式中的 *(volatile unsigned int *)0x500 解释
C语言中*(volatile unsigned int *)0x500的解释: 如下: (unsigned int *)0x500:将地址0x500强制转化为int型指针*(unsigned int ...
- VC++中几种字符标志的解释
VC++中几种字符标志的解释 LPSTR = char * LPCSTR = const char * LPWSTR = wchar_t * LPCWSTR = const wchar_t * LPO ...
- 31 Python中 sys.argv[]的用法简明解释(转)
Python中 sys.argv[]的用法简明解释 因为是看书自学的python,开始后不久就遇到了这个引入的模块函数,且一直在IDLE上编辑了后运行,试图从结果发现它的用途,然而结果一直都是没结果, ...
- Spring中IOC和AOP的详细解释(转)
原文链接:Spring中IOC和AOP的详细解释 我们是在使用Spring框架的过程中,其实就是为了使用IOC,依赖注入,和AOP,面向切面编程,这两个是Spring的灵魂. 主要用到的设计模式有工厂 ...
- [js]js的惰性声明, js中声明过的变量(预解释),后在不会重新声明了
js的惰性声明, js中声明过的变量(预解释),后在不会重新声明了 fn(); // 声明+定义 js中声明过一次的变量,之后在不会重新声明了 function fn() { console.log( ...
- Functor and Monad in Swift
I have been trying to teach myself Functional Programming since late 2013. Many of the concepts are ...
- Python中 sys.argv的用法简明解释
Python中 sys.argv[]的用法简明解释 sys.argv[]说白了就是一个从程序外部获取参数的桥梁,这个“外部”很关键,所以那些试图从代码来说明它作用的解释一直没看明白.因为我们从外部取得 ...
随机推荐
- centos 配置svn http serve
你看到的这个文章来自于http://www.cnblogs.com/ayanmw 基本的安装包有: subversion /httpd/ svn的httpd的mod_dav_svn mod_authz ...
- case...when...then if 用法
select case when if 的一些用法 概述:sql语句中的case语句与高级语言中的switch语句,是标准sql的语法,适用于一个条件判断有多种值的情况下分别执行不同的操作. 首先,让 ...
- 20.基于es内部_version进行乐观锁并发控制
- JavaSE 学习笔记之接 口(六)
接 口: 1:是用关键字interface定义的. 2:接口中包含的成员,最常见的有全局常量.抽象方法. 注意:接口中的成员都有固定的修饰符. 成员变量:public static final ...
- 手写一个简化版Tomcat
一.Tomcat工作原理 我们启动Tomcat时双击的startup.bat文件的主要作用是找到catalina.bat,并且把参数传递给它,而catalina.bat中有这样一段话: Bootstr ...
- Spring Boot 定时任务单线程和多线程
Spring Boot 的定时任务: 第一种:把参数配置到.properties文件中: 代码: package com.accord.task; import java.text.SimpleDat ...
- Method and apparatus for an atomic operation in a parallel computing environment
A method and apparatus for a atomic operation is described. A method comprises receiving a first pro ...
- 51nod——T1103 N的倍数
题目来源: Ural 1302 基准时间限制:1 秒 空间限制:131072 KB 分值: 40 难度:4级算法题 收藏 关注 一个长度为N的数组A,从A中选出若干个数,使得这些数的和是N的倍数. ...
- Udp发送端和接收端
//UdpReceive.java /* 定义udp的接收端. 思路: 1.定义udpSocket服务.一般会监听一个端口,事实上就是这个接收网络应用程序定义一个数字标示. 2.定义一个数据包.用来存 ...
- webbench压力測试工具
apache的測试工具ab 在并发100个以上后会出现错误.网上也有非常多改ab源代码来解禁的. 只是还是推荐一款比較好用的压力測试工具webbench wget http://blog.zyan.c ...