一个Monad的不严谨介绍
一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已,这有什么难以理解的?*
之前了解了下Monad,后来一段时间没碰,最近研究Parser用到Monad时发现又不懂了。现在重新折腾,趁着记忆还热乎,赶紧写下来。本文不会完整讲解Monad,而只介绍Monad相关的思想与编程技巧。
不要被唬人的数学概念吓唬到了。对于程序员来说,Monad不过就是一种编程技巧,或者说是一种设计模式。 Monad并非Haskell特有。实际上,大部分语言都有应用过Monad的思想。下面我将主要使用Scheme来解释Monad。
Monad是什么
Monad是一种数据类型,它有以下两个特点:
Monad封装了一个值。
这个封装的含义比较广义,它既可以是用数据结构包涵了一个值,也可以是一个函数(通过返回值来表达被封装的值)。所以一般也说Monad是一个“未计算的值”、“包含在上下文(context)中的值”。
存在两个Monad相关的函数: 提升(
return
函数)与绑定(>>=
函数)。-- 提升 --
return :: a -> M a
-- 绑定 --
>>= :: M a -> (a -> M b) -> M b
代码中
a
、b
表示两种数据类型,M a
表示封装了a
类型的Monad类型,M b
表示封装了b
类型的Monad类型。提升函数将一个值封装成一个Monad。而绑定函数就像一个管道,它解封一个Monad,将里面的值传到第二个参数表示的函数,生成另一个Monad。
以上是一个粗浅的定义。想要进一步了解的朋友可以去查看维基的Monad词条。
另外有一点要注意,Monad的两个操作中的提升操作做了封装,但是并没有提供解封的操作(M a -> a
类型的操作)。下图展示了Monad两个操作的关系:
下面我们来看看Monad的应用。
Maybe
Maybe是最简单,也是最常被提起的一个例子。Maybe类似C#中的Nullabe类型,表示有一个值,或者没有值。我们可以在Scheme这样表示Maybe类型:
; 有一个值
(define (just a) `(Just ,a))
; 没有值
(define nothing 'Nothing)
可以看到,Maybe类型封装了值a
,只缺提升和绑定操作就可以作为Monad了。定义提升和绑定如下:
; 提升
(define return just)
; 绑定
(define (>>= ma f)
(if (eq? ma nothing)
nothing
(f (cadr ma))))
接下来我们看一个求倒数的例子。我们定义一个inv
函数,该函数接收一个数字x
作为参数。当x
等于0
时,输出Nothing
;当x
不为0
时,计算x
的倒数1/x
,并封装为(Just 1/x)
。
(define (inv x)
(if (zero? x) nothing (return (/ 1.0 x))))
定义完inv
后,我们就能通过>>=
将它应用到Maybe类型来求倒数了。测试一下:
(pretty-print (>>= (just 10) inv))
; > (Just 0.1)
(pretty-print (>>= (just 0) inv))
; > Nothing
(pretty-print (>>= nothing inv))
; > Nothing
Maybe这个例子还揭示了为什么Monad没有粗暴地提供一个解封的函数:并非所有Monad都能解封,(Just a)
能解封,但是Nothing
不能解封!因此只能通过绑定函数来访问封装里面的值。
状态
Monad最出名的用法是模拟状态。众所周知,Haskell是一门纯函数语言,因而Haskell不得不大量使用Monad来模拟副作用。然而,Monad也仅仅是模拟,而非真正实现了副作用。应用了Monad技巧的函数仍然是纯函数。王垠在他的《对函数式语言的误解》准确了描述了Monad模拟副作用的本质:
为了让 random 在每次调用得到不同的输出,你必须给它“不同的输入”。那怎么才能给它不同的输入呢?Haskell 采用的办法,就是把“种子”作为输入,然后返回两个值:新的随机数和新的种子,然后想办法把这个新的种子传递给下一次的 random 调用。
现在问题来了。得到的这个新种子,必须被准确无误的传递到下一个使用 random 的地方,否则你就没法生成下一个随机数。因为没有地方可以让你“暂存”这个种子,所以为了把种子传递到下一个使用它的地方,你经常需要让种子“穿过”一系列的函数,才能到达目的地。种子经过的“路径”上的所有函数,必须增加一个参数(旧种子),并且增加一个返回值(新种子)。这就像是用一根吸管扎穿这个函数,两头通风,这样种子就可以不受干扰的通过。
为了减轻视觉负担和维护这些进进出出的“状态”,Haskell 引入了一种叫 monad 的概念。它的本质是使用类型系统的“重载”(overloading),把这些多出来的参数和返回值,掩盖在类型里面。这就像把乱七八糟的电线塞进了接线盒似的,虽然表面上看起来清爽了一些,底下的复杂性却是不可能消除的。
虽然用Monad模拟状态既复杂、用处也不多,但是学习一下既有乐趣又不乏启发,所以姑且来看一下事情是怎么做的。
为了调试与演示方便,我们这里不用random
函数作为例子,而是实现一个sequence
函数。该函数不接收参数,每次调用的返回值都是上一次的返回值加1。
我们先考虑没有使用Monad的情况。在这种情况下,sequence
函数以及其他所有相关的函数需要一个状态参数,并返回返回值与新状态两个值。现在我们考虑Monad的类型。我们要把返回的新状态隐藏起来,很自然的思路就是将新状态当作用来封装返回值的Monad壳子(也可以理解为这个新状态表达了一个上下文)。用一个pair来表示这个封装:
(cons value new-state)
另外,还有一个要隐藏的,就是输入到函数的状态参数。如何将参数隐藏到Monad比较费脑。事实上,在我们编写函数代码时,我们根本就不知道这个状态参数是从哪里传过来的,我们对状态参数一无所知。既然我们对这个状态参数一无所知,那我们对这个状态参数的处理就是先不处理,等程序执行到这里的时候再计算(这有点像惰性求值,联想下非惰性求值语言是怎么实现惰性求值的?),也就是说,我们要把与状态参数相关的计算过程整个封装起来,只有获取到状态参数时才能解封得到实际的值。用什么来表示“计算过程”呢?答案是函数(lambda)。到这里就清晰了,要同时隐藏返回值、返回的新状态以及状态参数,我们需要的Monad类型是个函数类型,它大概长这个样子:
old-state -> (cons value new-state)
; type: number -> number * number
接下来定义提升函数,提升函数返回输入的值i
,并保持状态不变:
(define (return i)
(lambda (state) (cons i state)))
绑定函数先利用状态参数state
解封m
计算得m
中的值与新状态,再将f
应用到解封得到的值和新的状态
(define (>>= m f)
(lambda (state)
(let ([p (m state)])
((f (car p)) (cdr p)))))
为了实现sequence
函数,我们还需要一个获取状态的函数get-state
和一个“设置”状态的函数set-state
。get-state
返回状态值并保持状态不变。set-state
接收一个参数,将状态设置为该参数,并返回(void)
。代码如下:
(define (get-state)
(lambda (state) (cons state state)))
(define (set-state state)
(lambda (old-state) (cons (void) state)))
万事俱备!可以来实现sequence
了。sequence
依次做了以下事情:
- 获取状态
state
- 设置新状态为
state+1
- 返回
state+1
代码如下:
(define (sequence)
(>>= (get-state)
(lambda (state)
(>>= (set-state (+ state 1))
(lambda (_)
(return (+ state 1)))))))
为了简化嵌套回调,我写了一个宏来处理嵌套回调:
(define-syntax do/m
(syntax-rules (<-)
[(_ bind e) e]
[(_ bind (v <- e0) e e* ...)
(bind e0 (lambda (v)
(do/m bind e e* ...)))]
[(_ bind e0 e e* ...)
(bind e0 (lambda (_)
(do/m bind e e* ...)))]))
这样sequence
的实现可以简化为:
(define (sequence1)
(do/m >>=
(state <- (get-state))
(set-state (+ state 1))
(return (+ state 1))))
有没有很像命令式的写法?下面来测试一下:
; 方便展示用的辅助函数,请忽视它是个有副作用的函数。
(define (printi v) (return (pretty-print v)))
(define run-program
(do/m >>=
(i1 <- (sequence))
(i2 <- (sequence))
(printi i1)
(printi i2)
(i3 <- (sequence))
(printi i3)))
注意到这里的Monad是一个接受状态参数的函数,我们要传入初始的状态参数来让这段代码真正跑起来。我们传入初始状态0
:
(run-program 0)
;output:
; > 1
; > 2
; > 3
其他应用
Continuation
熟悉continuation的朋友可以看出continuation也是一种Monad。
JavaScript
根据JavaScript面向对象的特性,绑定函数可以定义为Monad的一个方法。下面定义了一个简单的Monad类型,它单纯封装了一个值作为value
属性:
var Monad = function (v) {
this.value = v;
return this;
};
Monad.prototype.bind = function (f) {
return f(this.value)
};
var lift = function (v) {
return new Monad(v);
};
我们将一个除以2的函数应用的这个Monad:
console.log(lift(32).bind(function (a) {
return lift(a/2);
}));
// > Monad { value: 16 }
是不是有点像Promise?
连续应用除以2的函数:
// 方便展示用的辅助函数,请忽视它是个有副作用的函数。
var print = function (a) {
console.log(a);
return lift(a);
};
var half = function (a) {
return lift(a/2);
};
lift(32)
.bind(half)
.bind(print)
.bind(half)
.bind(print);
//output:
// > 16
// > 8
这是链式编程。
结尾
Monad虽然曲高和寡,但其思想悄悄地融入到了各个语言中。本文到此结束,希望对你能有所帮助。
相关链接
一个Monad的不严谨介绍的更多相关文章
- 微软在 .NET 3.5 新增了一个 HashSet 类,在 .NET 4 新增了一个 SortedSet 类,本文介绍他们的特性,并比较他们的异同。
微软在 .NET 3.5 新增了一个 HashSet 类,在 .NET 4 新增了一个 SortedSet 类,本文介绍他们的特性,并比较他们的异同. .NET Collection 函数库的 Has ...
- 创建我们第一个Monad
上一篇中介绍了如何使用amplified type, 如IEnumerable<T>,如果我们能找到组合amplified type函数的方法,就会更容易写出强大的程序. 我们已经说了很多 ...
- 一个分门别列介绍JavaScript各种常用工具的脑图
博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:一个分门别列介绍JavaScript各种常用工具的脑图.
- WPF: WpfWindowToolkit 一个窗口操作库的介绍
在 XAML 应用的开发过程中,使用MVVM 框架能够极大地提高软件的可测试性.可维护性.MVVM的核心思想是关注点分离,使得业务逻辑从 View 中分离出来到 ViewModel 以及 Model ...
- Grafana是一个可视化面板-安装配置介绍
Grafana是一个可视化面板(Dashboard),有着非常漂亮的图表和布局展示,功能齐全的度量仪表盘和图形编辑器,支持Graphite.zabbix.InfluxDB.Prometheus和Ope ...
- 介绍一个简单的Parser
我们已经学习了怎样创建一个简单的Monad, MaybeMonad, 并且知道了它如何通过在 Bind函数里封装处理空值的逻辑来移除样板式代码. 正如之前所说的,我们可以在Bind函数中封装更复杂的逻 ...
- 翻译连载 | 附录 B: 谦虚的 Monad-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇
原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...
- Scalaz(25)- Monad: Monad Transformer-叠加Monad效果
中间插播了几篇scalaz数据类型,现在又要回到Monad专题.因为FP的特征就是Monad式编程(Monadic programming),所以必须充分理解认识Monad.熟练掌握Monad运用.曾 ...
- Scalaz(10)- Monad:就是一种函数式编程模式-a design pattern
Monad typeclass不是一种类型,而是一种程序设计模式(design pattern),是泛函编程中最重要的编程概念,因而很多行内人把FP又称为Monadic Programming.这其中 ...
随机推荐
- 使用opencv实现自定义卷积
对图像进行卷积是图像处理的基本操作,最近在研究图像滤波,经常要用到自定义卷积,所以实现了一下 #include "opencv2/imgproc/imgproc.hpp" #inc ...
- 为JQuery EasyUI 表单组件增加“焦点切换”功能
1.背景说明 在使用 JQuery EasyUI 各表单组件时,实际客户端页面元素是由 JQuery EasyUI 生成的,元素的焦点切换,虽然 Tab 键可以正常用,但顺序控制属性 tabinde ...
- PHP7中我们应该学习会用的新特性
PHP7于2015年11月正式发布,本次更新可谓是PHP的重要里程碑,它将带来显著的性能改进和新特性,并对之前版本的一些特性进行改进.本文小编将和大家一起来了解探讨PHP7中的新特性. 1. 标量类型 ...
- Jquery对复选框CheckBox的操作
checkbox: 多选框 //获取选中值 checkbox:$("#checkbox_id").attr("value"): 多选框checkbox,打勾: ...
- Winform 使用DotNetBar 根据菜单加载TabControl
winform 如何使用TabControl 控件来做winform界面框架? 这样的效果: 首先菜单的窗口展示的承载器为TabControl 控件,这个控件本身包含多页面预览和页面初始化. 如图所示 ...
- 用NodeJS创建一个聊天服务器
Node 是专注于创建网络应用的,网络应用就需要许多I/O(输入/输出)操作.让我们用Node实现有多么简单,并且还能轻松扩展. 创建一个TCP服务器 var net = require('net') ...
- hdu4027线段树
https://vjudge.net/contest/66989#problem/H 此题真是坑到爆!!说好的四舍五入害我改了一个多小时,不用四舍五入!!有好几个坑点,注意要交换l,r的位置,还有输出 ...
- oracle linux 6.5 安装 oracle 12cR2数据库(2)-DBCA建库
援引:http://www.cnblogs.com/kerrycode/p/3386917.html by 潇湘隐者 Oracle 12C引入了CDB与PDB的新特性,在ORACLE 12C数据库引 ...
- 大数据和BI商业智能有何区别?有何相关?
大数据 ≠BI商业智能,大数据也不是传统商业智能的简单升级. 1.大数据和BI两者的区别 BI(BusinessIntelligence)即商业智能,它是企业数据化管理的一整套的方案,用来将企业中现有 ...
- 使用fontawesome图标
我每次找图标时都是在阿里的开源图标库中找的,但是使用起来不是很方便.而我发现了fontawesome之后,觉得实在不错,所以分享给大家. 这是一些参考的文档. fontawesome下载与使用介绍 ...