函数编程中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[]说白了就是一个从程序外部获取参数的桥梁,这个“外部”很关键,所以那些试图从代码来说明它作用的解释一直没看明白.因为我们从外部取得 ...
随机推荐
- swift-计算字符串长度
text.characters.count(记得text一定要是String类型)
- 51nod1242斐波那契数列的第N项 【矩阵快速幂】
斐波那契数列的定义如下: F(0) = 0 F(1) = 1 F(n) = F(n - 1) + F(n - 2) (n >= 2) (1, 1, 2, 3, 5, 8, 13, 21, 34, ...
- SSH(远程登录)
在linux中SSH服务对应两个配置文件: ssh特点:在传输数据的时候,对文件加密后传输. ssh作用:为远程登录会话和其他网络服务提供安全性协议. ssh小结: 1.SSH是安全的加密协议,用于远 ...
- 阿里云对象存储服务,OSS使用经验总结,图片存储,分页查询
阿里云OSS-使用经验总结,存储,账号-权限,分页,缩略图,账号切换 最近项目中,需要使用云存储,最后选择了阿里云-对象存储服务OSS.总的来说,比较简单,但是仍然遇到了几个问题,需要总结下. 1.O ...
- 单元测试,我在公司Web团队的分享
一.单元测试的意义 1.质量 2.效率 (短期和长远都值得)写单元测试代码,总的来说其实是更节省开发时间,更保证质量的.Controller.Service.Dao其实都可以进行测试. 通过启动 To ...
- mysql如何删除数据库指定ID段的数据库。比如删除id 1-500的数据。
delete from tablename where id>=1 and id<=500或者DELETE FROM `数据库名称`.`数据表名称` WHERE `house_cs`.`i ...
- jsonp 后台返回注意事项
前端代码 <script src="http://apps.bdimg.com/libs/jquery/1.9.1/jquery.min.js"></script ...
- (25)Spring Boot使用自定义的properties【从零开始学Spring Boot】
spring boot使用application.properties默认了很多配置.但需要自己添加一些配置的时候,我们应该怎么做呢. 若继续在application.properties中添加 如: ...
- EF--复杂类型
介绍EF复杂类型的文章 我理解的复杂类型就是简化了编码的操作,实际上在数据库中还是按照约定生成相应的类似"类名_类名"的表结构 public class CompanyAddres ...
- caffe中的前向传播和反向传播
caffe中的网络结构是一层连着一层的,在相邻的两层中,可以认为前一层的输出就是后一层的输入,可以等效成如下的模型 可以认为输出top中的每个元素都是输出bottom中所有元素的函数.如果两个神经元之 ...