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...

Scala和范畴论 -- 对Monad的一点认识的更多相关文章

  1. 浅释Functor、Applicative与Monad

    引言 转入Scala一段时间以来,理解Functor.Applicative和Monad等概念,一直是我感到头疼的部分.虽然读过<Functors, Applicatives, And Mona ...

  2. Scalaz(11)- Monad:你存在的意义

    前面提到了scalaz是个函数式编程(FP)工具库.它提供了许多新的数据类型.拓展的标准类型及完整的一套typeclass来支持scala语言的函数式编程模式.我们知道:对于任何类型,我们只需要实现这 ...

  3. Scalaz(32)- Free :lift - Monad生产线

    在前面的讨论里我们提到自由数据结构就是产生某种类型的最简化结构,比如:free monoid, free monad, free category等等.我们也证明了List[A]是个free mono ...

  4. 【scala】类的定义和单例对象

    一.类的定义 Scala类的定义跟JAVA中类的定义基本是一样的. 示例 class ChecksumAccumulator{ var sum = 0;//Scala默认关键字为public priv ...

  5. Python爬取CSDN博客文章

    0 url :http://blog.csdn.net/youyou1543724847/article/details/52818339Redis一点基础的东西目录 1.基础底层数据结构 2.win ...

  6. [Spark性能调优] 第二章:彻底解密Spark的HashShuffle

    本課主題 Shuffle 是分布式系统的天敌 Spark HashShuffle介绍 Spark Consolidated HashShuffle介绍 Shuffle 是如何成为 Spark 性能杀手 ...

  7. 彻底解密 Spark 的 HashShuffle

    本课主题 Shuffle 是分布式系统的天敌 Spark HashShuffle介绍 Spark Consolidated HashShuffle介绍 Shuffle 是如何成为 Spark 性能杀手 ...

  8. scala try monad

    当输入的数据格式不正确时,ActivityData 中会出现 OutofIndex 错误,但更多的时候我们只关心想要的结果而不想了解出现了怎样的错误,然后会写出这样的代码   def parseCSV ...

  9. scala(9) Monad

    一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已.这句话涉及到了几个概念:单子(Monad),自函子(Endo-Functor),幺半群(Monoid),范畴(category). 范 ...

随机推荐

  1. UVALive 4222 /HDU 2961 Dance 大模拟

    Dance Problem Description For a dance to be proper in the Altered Culture of Machinema, it must abid ...

  2. Found conflicts between different versions of the same dependent assembly that could not be resolved

    https://stackoverflow.com/questions/24772053/found-conflicts-between-different-versions-of-the-same- ...

  3. JAVA设计模式之【状态模式】

    状态模式 水.固态.气态.液态 账户.正常状态.透支状态.受限状态 状态模式中,用一个状态类来分散冗长的条件语句,让系统有灵活性和可扩展性 状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的 ...

  4. 【转】Android 服务器之SFTP服务器上传下载功能 -- 不错

    原文网址:http://blog.csdn.net/tanghua0809/article/details/47056327 本文主要是讲解Android服务器之SFTP服务器的上传下载功能,也是对之 ...

  5. ubuntu修改顶栏颜色

    title: ubuntu修改顶栏颜色 toc: false date: 2018-09-29 19:14:01 categories: methods tags: Ubuntu 编辑shell主题的 ...

  6. 简单的beego分页功能代码

    一个简单的beego分页小插件(源代码在最下面): 支持条件查询 支持参数保留 支持自定义css样式 支持表/视图 支持参数自定义 默认为pno 支持定义生成链接的个数 使用方式: 1)action中 ...

  7. SVN 部分常用命令

    1. svn status 提交前显示出本地文本和版本库文本的区别 [url=] L abc.c # svn已经在.svn目录锁定了abc.c M bar.c # bar.c的内容已经在本地修改过了 ...

  8. 转:利用 T-sql 的从句 for xml path('') 实现多行合并到一行, 并带有分隔符

    http://blog.csdn.net/rav009/article/details/50723307 T-sql 有一个for xml path('')的从句能把多行结果合并到一行,并成为xml ...

  9. 【原创】TimeSten安装与配置

    1.安装TimeSten 2.安装时要指定TNS_ADMIN_LOCATION,即tnsnames.ora的路径,因为tt会根据这个连接Oracle.C:\TimesTen\tt1122_32\net ...

  10. javascript常用代码(不完整版)

    求大神指点 Javascript嵌入式 <script typt:javascript>代码</script> 注释 //或者/*内容*/ 变量名赋值 Var 变量名 = 值 ...