当前位置: 首页 > 编程日记 > 正文

Scala和范畴论 -- 对Monad的一点认识

背景

所有一切的开始都是因为这句话:一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已,有什么难以理解的。第一次看到这句话是在这篇文章:程序语言简史(伪)。这句话出自Haskell大神Philip Wadler,也是他提议把Monad引入Haskell。Monad是编程领域比较难理解的概念之一,大部分人都是闻"虎"而色变,更不用说把它"收入囊中"了。我曾经好几次尝试去学习Monad,Functor等这些范畴论里的概念,最终都因为它太难理解,半途而废。

这次的开始完全是个误会。几周之前我开启了重温Scala的计划。当我看到Scala类型系统和Implicit相关章节时,遇到了Scala中比较重要的设计模式:类型类(type class)。于是想找一个大量使用了type class模式的开源类库学习其源码,以加深理解type class模式。Scalaz是个不错的选择。但是有一个问题,Scalaz是一个纯函数式的类库,学习它必然又会遇到Monad那些概念。好吧,再给自己一次机会。

概念篇

我们分析一下Philip这句话:一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已。这句话涉及到了几个概念:单子(Monad),自函子(Endo-Functor),幺半群(Monoid),范畴(category)。首先,我们先来把这些概念搞清楚。

范畴

范畴的定义

范畴由三部分组成:

  1. 一组对象。

  2. 一组态射(morphisms)。每个态射会绑定两个对象,假如f是从源对象A到目标对象B的态射,记作:f:A -> B

  3. 态射组合。假如h是态射f和g的组合,记作:h = g o f

下图展示了一个简单的范畴,该范畴由对象 A, B 和 C 组成,有三个单位态射 id_A, id_B 和 id_C ,还有另外两个态射 f : C => B 和 g : A => B 。

态射我们可以简单的理解为函数,假如在某范畴中存在一个态射,它可以把范畴中一个Int对象转化为String对象。在Scala中我们可以这样定义这个态射:f : Int => String = ...。所以态射的组合也就是函数的组合,见代码:

scala> val f1: Int => Int = i => i + 1
f1: Int => Int = <function1> scala> val f2: Int => Int = i => i + 2 f2: Int => Int = <function1> scala> val f3 = f1 compose f2 f3: Int => Int = <function1>

范畴公理

范畴需要满足以下三个公理。

  1. 态射的组合操作要满足结合律。记作:f o (g o h) = (f o g) o h

  2. 对任何一个范畴 C,其中任何一个对象A一定存在一个单位态射,id_A: A => A。并且对于态射g:A => B 有 id_B o g = g = g o id_A

  3. 态射在组合操作下是闭合的。所以如果存在态射 f: A => B 和 g: B => C ,那么范畴中必定存在态射 h: A => C 使得 h = g o f 

以下面这个范畴为例:

f 和 g 都是态射,所以我们一定能够对它们进行组合并得到范畴中的另一个态射。那么哪一个是态射 f o g 呢?唯一的选择就是 id_B 了。类似地,g o f=id_A 。

函子

函子定义

函子有一种能力,把两个范畴关联在一起。函子本质上是范畴之间的转换。比如对于范畴 C 和 D ,函子 F : C => D 能够:将 C 中任意对象a 转换为 D 中的 F(A);  将 C 中的态射 f : A => B 转换为 D 中的 F(f) : F(A) => F(B)

下图表示从范畴C到范畴D的函子。图中的文字描述了对象 A 和 B 被转换到了范畴 D 中同一个对象,因此,态射 g 就被转换成了一个源对象和目标对象相同的态射(不一定是单位态射),而且 id_A 和 id_B 变成了相同的态射。对象之间的转换是用浅黄色的虚线箭头表示,态射之间的转换是用蓝紫色的箭头表示。

单位函子

每一个范畴C都可以定义一个单位函子:Id: C => C。它将对象和态射直接转换成它们自己:Id[A] = A; f: A => B, Id[f] = f

函子公理

  1. 给定一个对象 A 上的单位态射Id_A , F(Id_A) 必须也是 F(A) 上的单位态射,也就是说:F(Id_A) = Id_(F(A))

  2. 函子在态射组合上必须满足分配律,也就是说:F(f o g) = F(f) o F(g)

自函子

自函子是一类比较特殊的函子,它是一种将范畴映射到自身的函子 (A functor that maps a category to itself)。

函子这部分定义都很简单,但是理解起来会相对困难一些。如果范畴是一级抽象,那么函子就是二级抽象。起初我看函子的概念时,由于其定义简单,并且我很熟悉map这种操作,所以一带而过。当看到Monad时,发现了一些矛盾的地方。返回头再看,当初的理解是错误的。所以,在学习这部分概念时,个人有一些建议:1. 函子是最基本,也是最重要的概念,这个要首先弄明白。本文后半部分有其代码实现,结合代码去理解。如何衡量已经明白其概念呢?脑补map的工作过程+自己实现Functor。2. 自函子也是我好长时间没有弄明白的概念。理解这个概念,可以参看Haskell关于Hask的定义。然后类比到Scala,这样会容易一些。

下边简单介绍群相关的概念。相比函子、范畴,群是相对容易理解的。

群的定义

群表示一个拥有满足封闭性、结合律、有单位元、有逆元的二元运算的代数结构。我们用G表示群,a,b是群中元素,则群可以这样表示:

  1. 封闭性(Closure):对于任意a,b∈G,有a*b∈G

  2. 结合律(Associativity):对于任意a,b,c∈G,有(a\b)\c=a\(b\c)

  3. 单位元或幺元 (Identity):存在幺元e,使得对于任意a∈G,e\a=a\e=a

  4. 逆元:对于任意a∈G,存在逆元a^-1,使得a^-1\a=a\a^-1=e

半群和幺半群

半群和幺半群都是群的子集。只满足封闭性和结合律的群称为半群(SemiGroup);满足封闭性,结合律同时又有一个单位元,则该群群称为幺半群

概念到此全部介绍完毕。数学概念定义通常都很简单,一句两句话搞定,但是由于其抽象程度高,往往很难理解。下边我们将通过Scala来实现其中的一些概念。

Scala和范畴论

大谈了半天理论,回到编程中来。对程序员来说,离开代码理解这些定义是困难的,没有实际意义的。

群的代码表示

由于实际应用中不会涉及到群,所以我们来看半群的代码表示。从上边的概念我们知道,半群是一组对象的集合,满应足封闭性和结合性。代码如下:

trait SemiGroup[A] {def op(a1: A, a2: A): A     }

A表示群的构成对象,op表示两个对象的结合,它的封闭性由抽象类型A保证。接着来看Monoid的定义,Monoid是SemiGroup的子集,并且存在一个幺元。代码如下:

trait Monoid[A] extends SemiGroup[A]{     def zero: A }

下边给出了三个例子,分别是string、list和option的幺半群实现。对于不同的幺半群群,它们的结合行为,和幺元是不一样的。当自己实现一个群时一定要注意这点。比如对于Int的幺半群,在加法和乘法的情况下幺元分别是0和1。

val stringMonoid = new Monoid[String] {def op(a1: String, a2: String) = a1 + a2  def zero = "" } def listMonoid[A] = new Monoid[List[A]] {  def op(a1: List[A], a2: List[A]) = a1 ++ a2  def zero = Nil } def optionMonoid[A] = new Monoid[Option[A]] {  def op(a1: Option[A], a2: Option[A]) = a1 orElse a2  def zero = None }

Functor的代码表示

trait Functor[F[_]] {def map[A, B](a: F[A])(f: A => B): F[B] } //list Functor的实现 def listFunctor = new Functor[List] {  def map[A, B](a: List[A])(f: (A) => B) = a.map(f) }

Functor代码是很简单的,但是,也不是特别容易理解(和其概念一样)。我在理解这段代码的时候又遇到了问题。第一个问题:A -> F[A]这个映射在哪里?第二个问题:A => B => F[A] => F[B]这个映射又体现在哪里?以下是我的理解:

  1. Functor的定义带有一个高阶类型F[ \ ]。在Scala里,像List[T],Option[T],Either[A, B]等这些高阶类型在实例化时必须要确定类型参数(把T,A,B这些类型称为类型参数)。所以,A->F[A]这条映射产生在F[ \ ]类型实例化的时候。List[Int]隐含了这样一条映射:Int => List[Int]。

  2. 要理解这个映射关系:A => B => F[A] => F[B],首先来看listFunctor.map的使用。map[Int, Int](List(1, 2, 3))(_ + 1),对于map它的入参是List(1, 2, 3),执行过程是List中的每一个元素被映射该函数_: Int + 1,得到的结果List(2, 3, 4)。所以,对于List这个范畴来说,这个过程其实就是:List[Int] => List[Int]。放眼到Int和List范畴,就是Int => Int => List[Int] => List[Int]

Monad

OK,该介绍的背景知识都说的差不多了。我们接下来看Monad。Monad的定义是这样的:Monad(单子)是从一类范畴映射到其自身的函子(天呐,和自函子的定义一模一样啊)。我们来看详细的定义:

Monad是一个函子:M: C -> C,并且对C中的每一个对象x以下两个态射:

  1. unit: x -> M[x]

  2. join/bind: M[M[x]] -> M[x]

第一个态射非常容易理解,但是第二个是什么意思呢?在解释它之前我们先来看一个例子:

scala> val s = Some(1) //1
s: Some[Int] = Some(1) scala> val ss = s.map(i => Some(i + 1)) //2 ss: Option[Some[Int]] = Some(Some(2)) scala> ss.flatten //3 res6: Option[Int] = Some(2) scala> val sf = s.flatMap(i => Some(i + 1)) //4 sf: Option[Int] = Some(2)

程序第二步,把Monad当做一个普通的函子执行map操作,我们得到了Some(Some(2)),然后执行flatten操作,得到了最终的Some(2)。也就是说,join就是map + flatten。接着看第四步,flatMap一次操作我们就得到了期望的结果。join其实就是flatMap。

接下来我们用Scala实现Monad的定义:

trait Monad[M[_]] {def unit[A](a: A): M[A]   //identity  def join[A](mma: M[M[A]]): M[A] }

还有一种更为常见的定义方式,在Scala中Monad也是以这种方式出现:

trait Monad[M[_]] {def unit[A](a: A): M[A]def flatMap[A, B](fa: M[A])(f: A => M[B]): M[B]
}

其实这两种定义方式是等价的,join方法是可以通过flatMap推导出来的:def join[A](mma: M[M[A]]): M[A] = flatMap(mma)(ma => ma)

结尾

不知道大家对Monad的概念有没有一个大概的了解了?其实它就是一个自函子。所以,当理解了函子的概念时,Monad已经掌握了百分之八九十。剩下的百分之十就是不断的练习和强化了。
那我们再回到Philip的这句话:一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已。该如何理解这句话?我就不再费劲去解释了,如果上边的概念都弄明白了,这句话自然也就明白了。另外,受限于个人的能力,及语言表达水平,文中难免有错误。为不影响大家追求真理,给出我学习时所参看的一些资源。

参看文档:
《Functional programming in scala》
http://stackoverflow.com/ques...
http://www.zhihu.com/question...
http://jiyinyiyong.github.io/...
http://hongjiang.info/scala/
http://yi-programmer.com/2010...
http://www.jdon.com/idea/mona...

相关文章:

Linux环境编程--linux中的perror、exit、_exit、wait 和 waitpid

perror&#xff1a;#include<stdio.h> #include<stdlib.h>定义函数 void perror(const char *s); perror ("open_port");函数说明 perror ( )用 来 将 上 一 个 函 数 发 生 错 误 的 原 因 输 出 到标 准 错误 (stderr) 。参数 s 所指的字符…

DeepMind 打造 AI 游戏系统,可以玩扑克、国际象棋、围棋等,战斗力爆表

编译 | 禾木木 出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09; 谷歌母公司 Alphabet 的人工智能实验室 DeepMind 长期以来一直投资于游戏人工智能系统。实验室的理念是&#xff0c;游戏虽然缺乏明显的商业应用&#xff0c;但却是认知和推理能力的独特相关挑战。这使…

WPF-动态加载

添加一个UserControl&#xff0c;命名为BusyIndicator&#xff0c;view为空&#xff0c;在其.cs文件中定义一个类 1 /// <summary> 2 /// 动态实体 3 /// </summary> 4 public class AnimationObject 5 { 6 publ…

ORA-06502 when awr report produce

最近在生成一套系统的AWR报告时出现了如下报错&#xff1a;ORA-06502: PL/SQL: numeric or value error: character string buffer too small&#xff0c;然后生成AWR报告的过程就终止了&#xff0c;查看生成的AWR报告&#xff0c;发现报告时不完整的&#xff0c;AWR报告到Comp…

进程间通信学习小结(共享内存)

要使用共享内存&#xff0c;应该有如下步骤&#xff1a;1.开辟一块共享内存 shmget()2.允许本进程使用共某块共享内存 shmat()3.写入/读出4.禁止本进程使用这块共享内存 shmdt()5.删除这块共享内存 shmctl()或者命令行下ipcrm 共享内存可以说是最有用的进程间通信方式&#xff…

[ObjectiveC]NSDATA, NSDICTIONARY, NSSTRING互转

2019独角兽企业重金招聘Python工程师标准>>> NSDATA-->NSDICTIONARY NSDictionary *dict [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; NSDICTIONARY-->NSDATA NSData *data [NSJSONSerialization dat…

如流智会2021:技术结合场景 让企业知识懂员工

12月10日&#xff0c;“如流智会2021智能进化”在京举行&#xff0c;学界专家业界大咖云集荟聚&#xff0c;共商企业智能化转型之道。会上&#xff0c;百度集团副总裁、百度集团首席信息官&#xff08;CIO&#xff09;李莹表示&#xff1a;“智能经济时代&#xff0c;智能组织是…

WSFC 仲裁模型选择

今天我们再来详细讨论下关于WSFC的仲裁模型&#xff0c;主要仲裁模型的优缺点&#xff0c;应该如何去思考选择最佳合适方案WSFC引入仲裁&#xff0c;主要有两个目的跟踪群集当前运作票数是否符合仲裁模型协定&#xff0c;如果低于最少允许节点&#xff0c;则决定关闭群集&#…

关于进程间通信的学习心得

进程&#xff1a;进程是指独立地址空间的指令序列进程的五种状态&#xff1a;新建&#xff0c;就绪&#xff0c;运行&#xff0c;睡眠&#xff0c;僵死进程间通信&#xff1a;是不同进程之间进行一些"接触"&#xff0c;这种接触有简单&#xff0c;有复杂。机制不同&a…

Go modules基础精进,六大核心概念全解析(上)

Go 语言做开发时&#xff0c;路径是如何定义的&#xff1f;Go Mudules又为此带来了哪些改变&#xff1f;本文将会全面介绍Go Modules六大核心概念&#xff0c;包括了设计理念与兼容性原则等&#xff0c;掌握这些技术点对于管理和维护Go 模块有重要价值。 在Go Modules 的前世今…

PARAMETERS 指令

语法: PARAMETERS <p> [DEFAULT <f>] [LOWER CASE] [OBLIGATORY] [AS CHECKBOX] [RADIOBUTTON GROUP <rad>] 实例: PARAMETERS: NAME(8), AGE TYPE I, BIRTH TYPE D. OBLIGATORY:强制要求输入, 屏幕上会出現一个“√” , 使用者必须要输入才可。 AS C…

阿里发布AliGenie2.0系统,“百箱大战”用上视觉武器

天猫精灵X1的升级版X2没有预期出现&#xff0c;而人机交互系统AliGenie升级到最新的2.0版本&#xff0c;功能强大。 3月22日&#xff0c;阿里巴巴人工智能实验室总经理浅雪&#xff08;陈丽娟&#xff09;发布AliGenie2.0系统&#xff0c;它最大的改进是在1.0的基础上增加了视觉…

Centos5.6 VNC安装配置【无错版】

不严格按本步骤就会出现VNC桌面花屏&#xff0c;就是桌面分离为一层一层的。。。 ---------------------------------------- 先装X window http://blog.csdn.net/21aspnet/article/details/6997549 ---------------------------------------- Centos5.6 VNC安装配置 一、检查是…

关于IOS的屏幕适配(iPhone)——资源适配

IOS的屏幕适配几乎不需要大量的代码操作&#xff0c;更多的时间我们只是动动鼠标选择一下就搞定。可以苹果在这方面做的还是比较人性的&#xff0c;解放了开发者。 首先来说说Iphone这几种屏&#xff08;由于最近做的是iPhone APP还未涉及到iPad&#xff0c;将来涉及到iPad时会…

Go modules基础精进,六大核心概念全解析(下)

Go 语言做开发时&#xff0c;路径是如何定义的&#xff1f;Go Mudules又为此带来了哪些改变&#xff1f;本文将会全面介绍Go Modules六大核心概念&#xff0c;包括了设计理念与兼容性原则等&#xff0c;掌握这些技术点对于管理和维护Go 模块有重要价值。 在上篇中&#xff0c;我…

京东区块链白皮书解读, 做“链接器”,一次技术宣言

前天&#xff0c;京东对外发布了《京东区块链技术白皮书(2018)》。 昨天&#xff0c;京东金融发布了旨在帮助中小银行提升零售信贷效率的产品“北斗”。目前&#xff0c;“北斗”已经接入包括江苏银行、南京银行、包商银行在内的近30家银行。京东金融还与近30家商业银行共同发起…

xauth: (stdin):1: bad display name LSPPC-Lenny:1 in add command

启动vnc4server之后出现如下错误提示&#xff1a;LSPPC-Lenny:~# vnc4serverxauth: (stdin):1: bad display name "LSPPC-Lenny:1" in "add" command New ‘LSPPC-Lenny:1 (root)’ desktop is LSPPC-Lenny:1 Starting applications specified in /root/…

使用 Python 和 OpenCV 构建 SET 求解器

作者 | 小白来源 | 小白学视觉小伙伴们玩过 SET 吗&#xff1f;SET 是一种游戏&#xff0c;玩家在指定的时间竞相识别出十二张独特纸牌中的三张纸牌&#xff08;或 SET&#xff09;的模式。每张 SET 卡都有四个属性&#xff1a;形状、阴影/填充、颜色和计数。下面是一个带有一些…

Delphi XE5 常用功具与下载

1.Delphi XE5 正式版http://altd.embarcadero.com/download/radstudio/xe5/delphicbuilder_xe5_win.isohttp://altd.embarcadero.com/download/radstudio/xe5/delphicbuilder_xe5_upd1_win.iso2. cnpack 助手工具http://www.cnpack.org/download/unstable/CnWizards_1.0.1.665_…

maven学习(4)-Maven 构建Web 项目

紧接着上一节(3)&#xff0c;现在maven新建web项目&#xff0c;user-web。模拟一个用户登录的需求&#xff1a; 工程结构&#xff1a; pom.xml: <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&qu…

如何查看linux版本

1. 查看内核版本命令&#xff1a; 1) [rootq1test01 ~]# cat /proc/version Linux version 2.6.9-22.ELsmp (bhcompilecrowe.devel.redhat.com) (gcc version 3.4.4 20050721 (Red Hat 3.4.4-2)) #1 SMP Mon Sep 19 18:00:54 EDT 2005 2) [rootq1test01 ~]# uname -a …

存储过程由结构表生成表

结构表 CREATE TABLE JGTB5001( ZDM VARCHAR2(30 BYTE), HZM VARCHAR2(100 BYTE), LX VARCHAR2(50 BYTE), JD VARCHAR2(20 BYTE), WBKLX VARCHAR2(100 BYTE), FUNCTIONNAME VARCHAR2(50 BYTE), FUNCTIONPARAMETER VARCHAR2(50 BYTE)); 生成的TB表CREATE OR REPLACE PROCEDURE P…

好礼相送|CSDN云原生 Meetup 成都站报名热烈启动,12.18见!

伴随着容器、Kubernetes及微服务等技术热度的持续攀升&#xff0c;云原生正以不可撼动之势&#xff0c;剑指云计算的下一个十年。12月18日&#xff0c;CSDN将在成都举办第三场云原生线下Meetup。在这里&#xff0c;您可以了解各大领先企业的云原生落地实践&#xff0c;与众多云…

vue-music 音乐网站

在学习完vueJS,一直想做个项目来锻炼一下,选来选去&#xff0c;还是做个网易云音乐&#xff0c;其间遇到了很多坑,也逐渐接受了vue这种组件化的思想以及从Dom操作转换为用数据去驱动视图。并且在某部分基础组件上借鉴(搬运)了elementUI的源码(不过elementUI写的是真好) 技术栈 …

shell环境变量

shell环境变量 环境变量 还记得上一章里面﹐我曾经提到过﹕当我们登入系统的时候﹐首先就获得一 shell﹐而且它也占据一个行程&#xff08;进程&#xff09;﹐然后再输入的命令都属于这个 shell 的子程序&#xff08;子进程&#xff09;。如果您学习够细心﹐不难发现我们的 sh…

apache用户认证

先创建一个“用户认证”目录&#xff08;设为abc&#xff09;[rootLAMPLINUX ~]# cd /data/www[rootLAMPLINUX www]# mkdir abc进入abc目录[rootLAMPLINUX www]# cd abc拷贝一个文件&#xff08;作用&#xff1a;验证配置是否生效&#xff09;[rootLAMPLINUX abc]# cp /etc/pas…

20个经典函数细说 Pandas 中的数据读取与存储,强烈建议收藏

作者 | 俊欣来源 | 关于数据分析与可视化大家好&#xff0c;今天小编来为大家介绍几个Pandas读取数据以及保存数据的方法&#xff0c;毕竟我们很多时候需要读取各种形式的数据&#xff0c;以及将我们需要将所做的统计分析保存成特定的格式。我们大致会说到的方法有&#xff1a;…

fastlane自动打包--详细介绍

fastlane--Packaging 自动化打包&#xff0c;通过fastlane自动发布Fastlane安装不在这里详细罗列&#xff0c;参照一下链接流程 https://www.jianshu.com/p/0a113f754c09操作步骤 1.检查Fastlane是否正确安装。输入以下命令&#xff1a; fastlane --version 复制代码可以看到Fa…

【Big Data】HADOOP集群的配置(一)

Hadoop集群的配置&#xff08;一&#xff09; 摘要: hadoop集群配置系列文档&#xff0c;是笔者在实验室真机环境实验后整理而得。以便随后工作所需&#xff0c;做以知识整理&#xff0c;另则与博客园朋友分享实验成果&#xff0c;因为笔者在学习初期&#xff0c;也遇到不少问题…

C语言 条件编译详解

预处理过程扫描源代码&#xff0c;对其进行初步的转换&#xff0c;产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。在C 语言中&#xff0c;并没有任何内在的机制来完成如下一些功能&#xff1a;在编译时包含其他源文件、定义宏、根据条件决定编译时是…