标签归档:monad

我所理解的monad(7):把monad看做行为的组合子

先从最简单的情况开始,我们用monad封装一段执行逻辑,然后提供一个map组合子,可以组合后续行为:

// 一个不完善的monad
class M[A](inner: => A) {

    // 执行逻辑
    def apply() = inner

    // 组合当前行为与后续行为,返回一个新的monad
    def map[B](next: A=>B): M[B] = new M(next(inner))
}

用上面的例子模拟几个行为的组合:

scala> val first = new M({println("first"); "111"})
first: M[String] = M@745b171c

scala> val second = first.map(x => {println("second"); x.toInt})
second: M[Int] = M@155f28dc

scala> val third = second.map(x => {println("third"); x + 200})
third: M[Int] = M@b345419

scala> third()
first
second
third
res0: Int = 311

看上去对行为的组合也不过如此,其实在Function1这个类里已经提供了对只有一个参数的函数的组合:

scala> class A; class B; class C

scala> val f1: A=>B = (a:A) => new B

scala> val f2: B=>C = (b:B) => new C

scala> val f3 = f1 andThen f2
f3: A => C = <function1>

Function1里的andThen方法可以把前面函数的结果交给后边的函数处理,A=>BB=>C 组成函数 A=>C

这样看上去我们当前实现的monad和Function1有些相似,封装了一段行为,提供方法将下一个行为组合成新的行为(可以不断的组合下去),在java里我们可以对比Command模式和Composite模式。

真实世界的”行为”(Action),普通函数的问题

1) 结果的不确定性,行为链的校验问题

这个不完善的monad已经可以做一些事情,但有个很显著的问题,只能组合”普通函数(plain)”,所谓普通函数是指结果类型就是我们想要的数据类型。

比如f: A=>B 这个函数可以表示一个行为,这个行为最终得到的数据类型是B,但如果这个行为遇到异常,或者返回为null,这时我们的组合过程会如何?

这意味着我们必须在组合过程中判断每个函数的结果是否合法,只有合法的情况下,才能与下一步组合。你可能会想把这些判断放在组合逻辑里不就得了吗?

def map[B](next: A=>B): M[B] = { 
        val result = inner  // 执行了当前行为
        if(result != null) {
            new M(next(result))
        }else {
            null.asInstanceOf[M[B]]
        }
    }

上面的情况不是我们希望的,因为要判断当前行为(inner)的结果是否符合传递给下一个行为,只有执行当前行为才能拿到结果。而我们希望组合子做的是先把所有行为组合起来,最后再一并执行。

看来只能要求next函数在实现的时候做了异常检测,但即使next函数做了判断,也不一定完美;因为行为已经先被组合好了,执行的时候,链上的每个函数仍会被调用。假设组合了多个函数,执行时中间的一个函数即使为null,仍会传递给下一个执行(下一个也必须也对参数检测),其实这个时候已经没有必要传递下去了

在scala里对这种结果不确定的情况,提供了一个专门的Option,它有两个子类:SomeNone,其中Some表示结果是一个正常值,可以通过模式匹配来获取到这个值,而None则表示为空的情况。

如果我们Option来表示我们前面的行为,可以写为:f: A=>Option[B],即结果被一个富类型给包装起来,它表示结果可能有值,也可能为空。

2) 副作用的问题

另外一个真实世界中无法用函数式完美解决的问题是IO操作,因为IO无论如何总要伴随状态的产生或变化(也就是副作用)

一段常见的举例片段:

def read(state: State) = {
    // 返回下一状态和读取到的字符串
    (state.nextState, readLine)
}

def write(state: State, str: String) = {
    //返回下一状态,和字符串处理的结果
    //这里是print返回为Unit类型,所以最终返回一个State和Unit的二元组
    (state.nextState, println(str))
}

这两个函数类型为State => (State, String)(State,String) => (State, Unit) 在入参和出参里都伴随State,是一种”不纯粹”的函数,因为每次都执行状态都不一样,即使两次的字符串是一样的,状态也是不同的。这是一种典型的“非引用透明”问题,这样的函数无法满足结合率,函数的组合性无法保障。

要实现组合特性,需要把函数转换为符合“引用透明”的特性,我们可以通过柯里化来把两个函数转化为高阶函数:

def read = 
    (state: State) => (state.nextState, readLine)

def write(str: String) = 
    (state: State) => (state.nextState, println(str))

现在这两个函数相当于只是构造了行为,要执行它们需要后续传入“状态”才行;等于把执行推迟了;同时这两个函数现在符合“引用透明”特性了。

再进一步,为了把State在构造行为的时候给隐藏起来,我们继续重构:

class IOAction[A](expression: =>A) extends Function1[State, (State,A)] { 

    def apply(s:State) = (state.next, expression) 
}

def read = new IOAction[String](readLine)

def write(str: String) = new IOAction[Unit](println(str))

现在我们可以把read看做是一个 () => IOAction[String] 的函数,write则是:String => IOAction[Unit]的函数。

用一个”富类型”表示行为的结果

我们看到现实世界的行为,除了可以用A=>B这样的plain function描述,还有A=>Option[B]A=>IOAction[B] 这种结果是一个富类型的函数来描述。我们把后两种统一描述为:

A => Result[B]

当我们要组合一个这种形式的行为时,不能再使用map,而是flatMap组合子。

实际上,我们一开始就提到过map并不是必须的方法,flatMap才是,可以把map看做只是一种特例。把行为都用统一形式A => Result[B] 来描述,对于普通的A=>B也可以转为A=>Result[B]

现在我们看几个flatMap的例子,先看一个Option的实际例子:

scala> Some({println("111"); new A}).
        flatMap(a => {println("222");Some(new B)}).
        flatMap(b => {println("333"); None}).
        flatMap(c => {println("444"); Some(new D)})
111
222
333
res14: Option[D] = None

上面的例子看到,在组合第三步的时候,得到None,后边的第四步c => {println("444"); Some(new D)}没有被执行。这是因为None与后续的行为结合时,会忽略后续的行为。

我所理解的monad(6):从组合子(combinator)说起

从同事的反馈了解到前边几篇monad的介绍说的有些抽象。现在还没有提出monad,如果把monoid与endofunctor结合起来,恐怕更是抽象。还是回到用大家更能体会的场景来描述,等对monad有了基本的印象之后,最后再试图把monad在现实世界的表现与范畴论里的形式结合起来。

我想到一个比较好的方式是从行为的组合来说monad,因为这个层面的例子容易接受,最近在用的spray-routing框架,它的核心Directive(指令)就是一个行为组合子monad

最初知道组合子(combinator)这个单词是从《PIS》书中专门有一章讲“连接符解析”(combinator parsing),当时没有理解combinator的概念,那章也是偏重说parser的;后来在很多资料里看到把monad也当成combinator对待,一直没有悟清楚各种combinator是不是一回事,直到看到了ajoo的系列文章(推荐一下):

论面向组合子程序设计方法: 
http://www.blogjava.net/ajoo/category/6968.html

论面向组合子程序设计方法 之一 创世纪
论面向组合子程序设计方法 之二 失乐园
论面向组合子程序设计方法 之三 失乐园之补充
论面向组合子程序设计方法 之四 燃烧的荆棘
论面向组合子程序设计方法 之五 新约
论面向组合子程序设计方法 之六 oracle
论面向组合子程序设计方法 之七 重构
论面向组合子程序设计方法 之八 monad
论面向组合子程序设计方法 之九 南无阿弥陀佛
论面向组合子程序设计方法 之十 还是重构
论面向组合子程序设计方法 之十一 微步毂纹生

ajoo是个牛人,早期在javaeye有看到过他实现了一套基于jvm的haskell,取名“jaskell”。那也是好些年前了,在javaeye围观他与Trustno1等人对函数式编程的讨论,云里雾里。现在感觉稍微能跟这些大牛们有一些对话,却找不到这样的圈子了。引用ajoo的话:

组合子,英文叫combinator,是函数式编程里面的重要思想。如果说OO是归纳法(分析归纳需求,然后根据需求分解问题,解决问题),那么 “面向组合子”就是“演绎法”。通过定义最基本的原子操作,定义基本的组合规则,然后把这些原子以各种方法组合起来。

引用另外一位函数式领域的大师的说法:

A combinator is a function which builds program fragments from program fragments

这句话有一些“自相似性”,可以把combinator先当成一种“胶水”,更具体一些,把scala的集合库里提供的一些函数看成combinator:

map
foreach
filter
fold/reduce
zip/partition
flatten/flatMap

而monad正是一个通用的combinator,也是一种设计模式,但它有很多面,就我后续的举例来说,先把monad看成一个“行为”的组合子。体现在代码上:

class M[A](value: A) {  

    // 1) 把普通类型B构造为M[B]
    // 因为M定义为class并提供了构造方法,可以通过new关键字来构造,该工厂方法可以省略
    def unit[B] (value : B) = new M(value)  

    // 2) 不是必须的
    def map[B](f: A => B) : M[B] = flatMap {x => unit(f(x))}  

    // 3) 必须,核心方法
    def flatMap[B](f: A => M[B]) : M[B] = ...  
} 

一个monad内部除了一个工厂方法unit(注意,与Unit类型没有关系,unit这里表示“装箱”,源自haskell里的叫法),还包含了两个combinator方法,mapflatMap,这两个组合子中flatMap是不可少的,map可以通过flatMap来实现。

unit方法不一定会定义在monad类内部,很多情况下会在伴生对象中通过工厂方法来实现。

这里忽略了monad里的法则,重点先理解M封装了什么?以及内部的mapflatMap的含义是什么。

我所理解的monad(5):自函子(Endofunctor)是什么

经过前面一篇对函子(Functor)的铺垫,我们现在可以看看什么是自函子(Endofunctor)了,从范畴的定义看很简单:

自函子就是一个将范畴映射到自身的函子 (A functor that maps a category to itself)

这句话看起来简单,但有个问题,如何区分自函子与Identity函子?让我们先从简单的“自函数”来看。

自函数(Endofunction)

自函数是把一个类型映射到自身类型,比如Int=>Int, String=>String

注意自函数与Identity函数的差异,Identity函数是什么也不做,传入什么参数返回什么参数,它属于自函数的一种特例;自函数是入参和出参的类型一致,比如 (x:Int) => x * 2(x:Int) => x * 3 都属于自函数:

自函子(Endofunctor)

自函子映射的结果是自身,下图是一个简单的情况:

假设这个自函子为F,则对于 F[Int] 作用的结果仍是Int,对于函数f: Int=>String 映射的结果 F[f] 也仍是函数f,所以这个自函子实际是一个Identity函子(自函子的一种特例),即对范畴中的元素和关系不做任何改变。

那怎么描述出一个非Identity的自函子呢?在介绍范畴在程序上的用法的资料里通常都用haskell来举例,把haskell里的所有类型和函数都放到一个范畴里,取名叫Hask,那么对于这个Hask的范畴,它看上去像是这样的:

先来解释一下(画这个图的时候做了简化),A,B代表普通类型如String,Int,Boolean等,这些(有限的)普通类型是一组类型集合,还有一组类型集合是衍生类型(即由类型构造器与类型参数组成的),这是一个无限集合(可以无限衍生下去)。这样范畴Hask就涵盖了haskell中所有的类型。

对于范畴Hask来说,如果有一个函子F,对里面的元素映射后,其结果仍属于Hask,比如我们用List这个函子:

List[A], List[List[A]], List[List[List[A]]]...

发现这些映射的结果也是属于Hask范畴(子集),所以这是一个自函子,实际上在Hask范畴上的所有函子都是自函子。

我们仔细观察这个Hask范畴的结构,发现它实际是一个fractal结构,所谓fractal(分形),是个很神奇的结构,在自然界也大量存在:

如上面的这片叶子,它的每一簇分支,形状上与整体的形状是完全一样的,即局部与整体是一致的结构(并且局部可以再分解下去)

这种结构在函数式语言里也是很常用的,最典型的如List结构,由headtail 两部分组合而成,而每个tail也是一个List结构,可以递归的分解下去。

我所理解的monad(4):函子(functor)是什么

大致介绍了幺半群(monoid)后,我们重新回顾最初引用wadler(haskell委员会成员,把monad引入haskell的家伙)的那句话:

一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已 

现在我们来解读这句话中包含的另一个概念:自函子(Endofunctor),不过我们先需要一些铺垫:

首先,什么是函子(Functor)?

乍一看名字,以为函子(functor)对函数(function)是一种封装,实际没有关系,尽管他们都是表示映射,但两者针对的目标不一样。

函数表达的映射关系在类型上体现在特定类型(proper type)之间的映射,举例来说:

// Int => String
scala> def foo(i:Int): String = i.toString

// List[Int] => List[String]
scala> def bar(l:List[Int]): List[String] = l.map(_.toString)

// List[T] => Set[T]
scala> def baz[T](l:List[T]): Set[T] = l.toSet

而函子,则是体现在高阶类型(确切的说是范畴,可把范畴简单的看成高阶类型)之间的映射(关于高阶类型参考: scala类型系统:24) 理解 higher-kinded-type),听上去还是不够直观,函子这个术语是来自群论(范畴论)里的概念,表示的是范畴之间的映射,那范畴又与类型之间是什么关系?

把范畴看做一组类型的集合

假设这里有两个范畴:范畴C1 里面有类型String 和类型 Int;范畴C2 里面有 List[String]List[Int]

函子表示范畴之间的映射

从上图例子来看,这两个范畴之间有映射关系,即在C1里的Int 对应在C2里的List[Int],C1里的String对应C2里的List[String],在C1里存在Int->String的关系态射(术语是morphism,我们可理解为函数),在C2里也存在List[Int]->List[String]的关系态射。

换句话说,如果一个范畴内部的所有元素可以映射为另一个范畴的元素,且元素间的关系也可以映射为另一个范畴元素间关系,则认为这两个范畴之间存在映射。所谓函子就是表示两个范畴的映射。

怎么用代码来描述函子?

从上图的例子,我们已经清楚了functor的含义,即它包含两个层面的映射:

1) 将C1中的类型 T 映射为 C2 中的 List[T] :  T => List[T]
2) 将C1中的函数 f 映射为 C2 中的 函数fm :  (A => B) => (List[A] => List[B])

要满足这两点,我们需要一个类型构造器

trait Functor[F[_]] {

    def typeMap[A]: F[A]

    def funcMap[A,B](f: A=>B): F[A]=>F[B] 
}

我们现在可以把这个定义再简化一些,类型的映射方法可以不用,并把它作为一个type class

trait Functor[F[_]] {
    def map[A,B](fa: F[A], f: A=>B): F[B]
}

现在我们自定义一个My[_]的类型构造器,测试一下这个type class:

scala> case class My[T](e:T)

scala> def testMap[A,B, M <: My[A]](m:M, f: A=>B)(implicit functor:Functor[My]) = {
 |          functor.map(m,f)
 |      }

scala> implicit object MyFunctor extends Functor[My] {
 |          def map[A,B](fa: My[A], f:A=>B) = My(f(fa.e))
 |      }

//对 My[Int], 应用函数 Int=>String 得到 My[String]
scala> testMap(My(200), (x:Int)=>x+"ok")
res9: My[String] = My(200ok)

不过大多数库中对functor的支持,都不是通过type class模式来做的,而是直接在类型构造器的定义中实现了map方法:

scala> case class My[A](e:A) {
     |     def map[B](f: A=>B): My[B] = My(f(e))
     | }

scala> My(200).map(_.toString)
res10: My[String] = My(200)

这样相当于显式的让My同时具备了对类型和函数的映射(A->My[A]A=>B -> My[A]=>My[B];在haskell里把这两个行为也叫提升(lift),相当于把类型和函数放到容器里),所以我们也可以说一个带有map方法的类型构造器,就是一个函子。

范畴与高阶类型

我们再来思考一下,如果忽略范畴中的关系(函数),范畴其实就是对特定类型的抽象,即高阶类型(first-order-type或higher-kinded-type,也就是类型构造器),那么对于上面例子中的”范畴C2″,它的所有类型都是List[T]的特定类型,这个范畴就可以抽象为List高阶类型。那对于”范畴C1″呢?它又怎么抽象?其实,”范畴C1″的抽象类型可以看做是一个Identity类型构造器,它与任何参数类型作用构造出的类型就是参数类型:

scala> type Id[T] = T

是不是很像单位元的概念?在shapeless里已经提起过

这么看的话,如果范畴也可以用类型(高阶)来表达,那岂不是只用普通函数就可以描述它们之间的映射了?别急,先试试,方法里是不支持类型构造器做参数的:

scala> def foo(cat: Id) = print(cat)
    <console>:18: error: type Id takes type parameters

方法中只能使用特定类型(proper type)做参数。

我所理解的monad(3):半群(semigroup)与并行运算

上文,因为半群里的“结合律”特性,使得我们可以对一些任务拆分采用并行处理,只要这些任务的结果类型符合“结合律”(即没有先后依赖)。让我们看一个单词统计的例子,阿里中间件团队前几个月有过一次编程比赛就是统计单词频度,参考:Coding4Fun第三期活动总结

编程比赛主要是考察技巧,现在我们看看现实世界中用scala怎么解决这个问题,如果数据是巨大的,无疑要采用map-reduce的思路,如果我们不依赖hadoop之类的系统来解决,最简单的方式就是actor处理,最后再汇总结果。这篇blog不讨论数据的统计处理,看看最后如何把这些结果合并

type Result = Map[String,Int]

object combiner extends SemiGroup[Result] {

    def append(x: Result, y: Result): Result = {
        val x0 = x.withDefaultValue(0)
        val y0 = y.withDefaultValue(0)
        val keys = x.keys.toSet.union(y.keys.toSet)
        keys.map{ k => (k -> (x0(k) + y0(k))) }.toMap
    } //不考虑效率
}

现在假设不同的actor分别返回r1,r2

val r1 = Map("hello" -> 1, "world" -> 2)
val r2 = Map("hello" -> 2, "ok" -> 5)

val counts = combiner.append(r1, r2)

如果有其他actor再返回结果的话,只要继续合并下去:

combiner.append(r3, counts)

twitter开源的algebird,就是基于semigroup/monoid的。不过我们不必沿着这条路深入下去,monad并不是为了并发而发明的,只是它正好是一个半群(幺半群),半群的结合律符合并行运算(由半群演化出来的迹幺半群和历史幺半群是进程演算和并行计算的基础)。这是另一个方向,不继续涉及下去,我们后续回到monad的其他特征上。

我所理解的monad(2):fold与monoid

在Scala中的核心数据结构List中,定义了fold操作,实际上这些方法是定义在scala集合库的最顶层特质GenTraversableOnce中的:

List中的左折叠(借用wiki上的图):

def foldLeft[B](z: B)(op: (B, A) => B): B

从图中可以看到,左折叠是用一个初始元素z从List的左边第一个元素开始操作,一直到对所有的元素都操作完。

现在我们对一个List进行累加操作:

scala> List("A","B","C").foldLeft("")(_+_)
res5: String = ABC

上面foldLeft传入的两个参数空字符串,以及二元操作函数 _+_ 不正好符合字符串monoid的定义吗?

object StringMonoid extends Monoid[String] {
    def append(a: String, b: String) = a + b
    def zero = ""
}

StringMonoid来代入:

scala> List("A","B","C").foldLeft(StringMonoid.zero)(StringMonoid.append)
res7: String = ABC

现在我们对List定义一个累加其元素的方法:

scala> def acc[T](list: List[T], m: Monoid[T]) = {
    list.foldLeft(m.zero)(m.append)
}

再进一步,把第二个参数改为隐式参数

scala> def acc[T](list: List[T])(implicit m: Monoid[T]) = { 
    list.foldLeft(m.zero)(m.append) 
}

现在Monoid成了一个type class,我们还可以再简化写法,用上下文绑定:

scala> def acc[T: Monoid](list: List[T]) = {
    val m = implicitly[Monoid[T]]
    list.foldLeft(m.zero)(m.append) 
}

如果我们在上下文提供了对应隐式值,就等于对List有了这种累加的能力:

scala> implicit val intMonoid = new Monoid[Int] { 
            def append(a: Int, b: Int) = a + b
            def zero = 0 
        }


scala> implicit val strMonoid = new Monoid[String] { 
            def append(a: String, b: String) = a + b
            def zero = ""
        }

scala> acc(List(1,2,3))
res10: Int = 6

scala> acc(List("A","B","C"))
res11: String = ABC

现在我们把Monoid看成基于二元操作(且提供单位元)的计算能力的抽象,不过仅仅是fold操作的话,还看不出它有什么威力。Monoid/SemiGroup中的结合律(associativity)特性才是它的威力所在,这个特性使得并行运算变得容易。

我所理解的monad(1):半群(semigroup)与幺半群(monoid)

google到数学里定义的群(group): G为非空集合,如果在G上定义的二元运算 *,满足

(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

则称(G,*)是群,简称G是群。

如果仅满足封闭性和结合律,则称G是一个半群(Semigroup);如果仅满足封闭性、结合律并且有幺元,则称G是一个含幺半群(Monoid)。

相比公式还是用代码表达更容易理解,下面表示一个半群(semigroup):

trait SemiGroup[T] {
    def append(a: T, b: T): T
}

特质SemiGroup,定义了一个二元操作的方法append,可以对半群内的任意2个元素结合,且返回值仍属于该半群。

我们看具体的实现,一个Int类型的半群实例:

object IntSemiGroup extends SemiGroup[Int] {
    def append(a: Int, b: Int) = a + b
}

// 对2个元素结合
val r = IntSemiGroup.append(1, 2)

现在在半群的基础上,再增加一个幺元(Identity,也翻译为单位元),吐槽一下,幺元这个中文不知道最早谁起的,Identity能表达的意义(同一、恒等)翻译到中文后完全消失了。

trait Monoid[T] extends SemiGroup[T] {
    // 定义单位元
    def zero: T
}

上面定义了一个幺半群,继承自半群,增加了一个单位元方法,为了容易理解,我们用zero表示,半群里的任何元素a与zero结合,结果仍是a本身。

构造一个Int类型的幺半群实例:

object IntMonoid extends Monoid[Int] {
    // 二元操作
    def append(a: Int, b: Int) = a + b
    // 单位元
    def zero = 0
}

构造一个String类型的幺半群实例:

object StringMonoid extends Monoid[String] {
    def append(a: String, b: String) = a + b
    def zero = ""
}

再构造一个复杂点的 List[T] 的幺半群工厂方法:

def listMonoid[T] = {
    new Monoid[List[T]] { 
        def zero = Nil
        def append(a: List[T], b: List[T]) = a ++ b 
    }
}

OK,现在我们已经了解了幺半群是什么样了,但它有什么用?

我所理解的monad(0)

以前在别人的一篇blog里看到过有这样一句话,大意是:关于monad,几乎每个在学习函数式编程中接触到这个模式的,都会写一篇博客描述他的理解。而且不同的人对monad的理解有所不同(暗讽monad的复杂)。

我也不例外,大约是3年前刚接触scala时,无意搜到了james的《monads are elephants》,一下子把我给难住了。scala在刚开始学习时,如果只把它当作java的替代者来用,继续用OO的范式的话,并不难。但在继续深入它的类型和函数式特征之后,发现有一篇广袤的世界是之前不曾看到的,可以以一种新的视角来思考编程的本质。而scala在OO与”新世界”之间架起一座桥(Bridge to Terabithia),通过这座桥的一个代价是,抛弃原先认为编程理所当然应该这样的观念。

在新的世界同样有设计模式,不过它们抽象的角度不同。理解起来有些困难,最大的原因是缺乏对这个新世界的认识,具体的说scala里最显著困难是它的类型系统,当类型的抽象度变高之后(犹如从二维世界到三维世界),它的含义和使用场景也会复杂很多。而monad正是基于高阶类型做的抽象。所以在了解monad前一定先要了解scala中高阶类型的概念,这块可以参考我blog中的scala类型系统系列。

通常monad也是一个标识,判断程序员对函数式世界的了解程度,它背后隐藏着很多的概念,并且很多来自数论(范畴领域),对于非科班出身的我来说,总是心存敬畏而避而远之,但我们如果去掉这些理论描述,仅从程序的实现来看,还是比较容易的,只要把这些概念一点点分解开的话。

在james的这篇 A Brief, Incomplete, and Mostly Wrong History of Programming Languages,(译文参考这里:程序语言简史(伪) ),有段关于monad的描述很有意思:

Haskell由于使用了Monad这种较费解的概念来控制副作用而遭到了一些批评意见。Wadler试图平息这些质疑,他解释说:“一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已,这有什么难以理解的?”

我们就从这句话说起,先理解是“幺半群”,别被这个字面术语吓到,其实用程序表达起来很简单。

翻译 monads-are-elephants 第三部分

原文:http://james-iry.blogspot.com/2007/10/monads-are-elephants-part-3.html

在这个系列我们提出了关于盲人与大象这个古老寓言的另一种观点。在这种观点里,从每个盲人那儿听取有限的解释,最终达到对大象很好的理解。

到目前我们还是站在monads的外面来看它。那让我们保持了很大的距离,现在可以看看它的内部了。真正决定大象成为大象的是它的DNA。Monads有它们自己的公共的DNA: monadic法则。

这篇文章要一下子消化它有点多,或许一段一段来读更有意义。在重读的时候把一个你已经理解的monad(例如List)带入规则会很有用。

Equality for All (相等性)

在我继续之前,我需要半正式的解释一下我在这些规则中使用(恒)等号的意思,比如 "f(x) ≡ g(x)" 数学上使用”=”表示相等性。我刻意避免使用”=”是为了避免与(用等号来)赋值相混淆。我是说左边的表达式与右边的表达式相同(is the same)。但现在个问题,什么是“相同”(same)。

第一,我不是在说引用一致(scala的eq方法),引用一致能满足我的定义,但这个要求太过强烈。第二,我不是必须要 == 或 equality,除非它的实现恰好是正确的。

我所说的”相同”是指两个对象在不直接或间接使用原始引用相等情况下是不可区分的。特别是,它可能对左边的表达式产生的对象与右边的(表达式产生的对象)内部存在微妙的差异,但仍可以说相同。例如,一个对象可能用一个迂回的间接层达到相同的结果。重要的是,从外部看,两个对象必须表现的一致。

对于“相同”还有点要注意的,我展示的所有的法则隐含假定没有副作用。对于副作用我会在文章的结尾更多描述。

Breaking the Law (打破法则)

不可避免的一些人会好奇“如果我打破法则会发生什么”?完整的答案取决于打破了什么法则,怎么打破的,我想先从整体上接近它。这儿再提醒一个其他来自数学分支的法则。

如果a,b,c是有理数,它们之间的乘法(*)遵循下面的法则:

a * 1 ≡ a
a * b ≡ b * a
(a * b) * c ≡ a * (b * c)

当然很容易创建一个有理数(RationalNumber)的类,并实现一个乘法(*)操作。但如果它不遵循这些法则,结果可能是令人困惑的。使用你的类的用户试着把数据塞入公式,却可能得到错的答案。坦白的说,很难打破这些法则却最终结果仍符合有理数乘法。

Monads不是有理数。但它们有法则帮助我们定义它们的操作方法。就像算数操作,它们(monads的操作方法)也有“公式”,你可以以有趣的方式来使用。例如,Scala的”for”表达式会用依赖这些法则的公式来进行展开。所以打破monad法则,就像打破”for”或使用者的一些其他预期。

介绍够了,开始解释monad法则,我将从另一个怪异的词语开始:函子(functor)

什么是函子(Functor)?

通常以”monad”或者”functor”开始的文章,很快就变成了一锅希腊字母。这是因为两个概念都来自一个叫做范畴论(category theory)的数学分支,完整的解释它们是一个数学练习。幸运的是,我的任务不是完整的解释它们而是用scala描述。

在Scala里functor是一个带有map方法和一些简单属性的类(class)。对于一个类型为M[A]的functor,map方法可以接受一个A=>B的函数,返回结果为M[B]。换句话说map用一个函数参数把M[A]转换为M[B]。重要的一点是要想到这个转换不需要任何循环,只要map就执行了转换。它可能是通过循环实现的,但也有可能不是。

Map的签名看上去像:

class M[A] {  
    def map[B](f: A => B):M[B] = ...  
}  
第一条Functor(函子)法则:Identity

我创建了一个名为identity的函数(译注: 这个identity就是幺半群里的单位元的概念)

def identity[A](x:A) = x  

这个明显对任何x都有这样的性质:

identity(x) ≡ x

它什么也没做,但这正是要点。它只是返回了参数(不管什么类型),不做任何改变。所以我们第一条functor法则,对于任何函子m:

F1. m map identity ≡ m // or equivalently *
F1b. m map {x => identity(x)} ≡ m // or equivalently
F1c. m map {x => x} ≡ m

换句话说,什么也没做,结果也不变。
聪明!不过,我要提醒你左边表达式可能返回一个内部结构与右边不同的对象。只要你没法分辨它们。
如果你创建一个functor不遵循这条法则,那么下面的将不能保证为true。看看为什么那样会导致困惑,假定m是一个List:

F1d. for (x <- m) yield x ≡ m
第二条Functor法则:Composition(组合)

第二条functor法则指定了几个”map”组合在一起的方式。

F2. m map g map f ≡ m map {x => f(g(x))} 

对m使用函数g进行map然后在对结果用函数f进行map,它与对m使用 f(g(x)) 来进行map是相同的。(译注:这也是幺半群里的结合律的概念)组合法则允许程序员把所有的事情一块儿干,或伸展到多个语句里。基于这条法则,程序员可以总假定下面的能工作:

val result1 = m map (f compose g)  
val temp = m map g  
val result2 =  temp map f  
assert result1 == result2  

在“for”表达式中,这条法则看起来让人眼睛疼。

F2b. for (y<- (for (x <-m) yield g(x)) yield f(y) ≡ for (x <- m) yield f(g(x))

Functors 与 Monads

你或许现在已经猜到,所有monads都是函子(functors),所以他们必须遵循functor法则。实际上,functor法则可以从monad法则中推导出来。只是因为functor法则简单且更容易搞定。

提醒,一个scala monad同时有map和flatMap方法,同下面的签名:

class M[A] {  
    def map[B](f: A => B):M[B] = ...  
    def flatMap[B](f: A=> M[B]): M[B] = ...  
}  

此外,我在这儿呈现的法则将基于”unit”。”unit”表示一个单个参数的构造器或类似下面签名的工厂方法:

def unit[A](x:A):M[A] = ...  

“unit”不应该被认为是字面名字的函数或方法,除非你想要。scala没有规定或使用它,但它是monads的重要一部分。任何满足这个签名和行为的函数,依据monad法则都可以。通常通过case类可以快捷的创建一个monad M,或者通过在这个类的伴生对象中定义个专用的apply(x:A):M[A]方法,这样M(x)的行为就是unit(x)

Functor/Monad连接法则:第零条法则

在这个系列的第一部分,我介绍过下面的关系:

FM1. m map f ≡ m flatMap {x => unit(f(x))}

这条法则单独来说没什么,但它可以在这三个概念间创建一个连接:unit, map, 和flatMap。这条法则可以使用”for”很好的表达:

FM1a. for (x <- m) yield f(x) ≡ for (x <- m; y <- unit(f(x))) yield y

回顾Flatten

在第一篇文章,我提到”flatten”或”join”的概念,用来转换一个monad的类型,M[M[A]]到M[A],但没有正式描述它。在那篇文章我说flatMap是一个map之后伴随flatten。

FL1. m flatMap f ≡ flatten(m map f)

这给出一个非常简单的flatten定义:

flatten(m map identity) ≡ m flatMap identity // 用identity替代f
FL1a. flatten(m) ≡ m flatMap identity // by F1

所以对m进行flattening 与对m用identity函数进行flatMapping是相同的。
在这篇文章我不会用flatten法则,因为flatten不是scala必须的,但这是个很好的概念,你可以放到你的后裤兜里,如果flatMap看起来太抽象的话。

第一条Monad法则:Identity

第一条也是最简单的monad法则是monad identity法则

M1. m flatMap unit ≡ m // or equivalently
M1a. m flatMap {x => unit(x)} ≡ m

连接器法则连接了3个概念,这条法则聚焦在它们两者之间的关系上。解读这条法则的一种方式是:某种意义上,flatMap总会把unit做的事情给还原。(译注:请google “monad与太空衣”,这个比喻很形象,unit的行为是给宇航员穿上太空衣,而flatMap则是脱掉) 再次提醒,左边结果的对象可能实际上内部有差异,只要它行为与”m”一致就行。

使用identity和连接法则,我们可以推导出functor的 identity 法则

m flatMap {x => unit(x)} ≡ m // M1a
m flatMap {x => unit(identity(x))}≡ m // identity
F1b. m map {x => identity(x)} ≡ m // by FM1

反过来推导也一样。在”for”表达式里monad identity法则非常直接。

M1c. for (x <- m; y <- unit(x)) yield y ≡ m

第二条Monad法则:Unit(构造方法)

Monads有一个逆向的monad identity法则:

M2. unit(x) flatMap f ≡ f(x) // or equivalently
M2a. unit(x) flatMap {y => f(y)} ≡ f(x)

这条法则基本上是说 unit(x) 必须以某种方式保存”x”,如果把函数f传给它,要能够计算出f(x)。正是在这个意义上,可以说任何monad都是一种类型的容器(但这不意味着monad是一个集合)。

在”for”表达式中,unit法则变为:

M2b. for (y <- unit(x); result <- f(y)) yield result ≡ f(x)

这条法则对unit以及它与map的关系,还有另一个含义

unit(x) map f ≡ unit(x) map f     // no, really, it does!
unit(x) map f ≡ unit(x) flatMap {y => unit(f(y))} // by FM1
M2c. unit(x) map f ≡ unit(f(x)) // by M2a

换句话说,如果我们通过一个参数”x”创建一个monad实例,然后传入函数f进行map;那么跟我们通过 f(x)的结果创建一个monad实例是一样的。在”for”表达式里

M2d. for (y <- unit(x)) yield f(y) ≡ unit(f(x))

第三条Monad法则:Composition(组合)

组合法则是一条怎么让一系列flatMap一起工作的规则。

M3. m flatMap g flatMap f ≡ m flatMap {x => g(x) flatMap f} // or equivalently

M3a. m flatMap {x => g(x)} flatMap {y => f(y)} ≡ m flatMap {x => g(x) flatMap {y => f(y) }}

这是所有法则中最难懂的,花点时间去领会。

在左边,我们对一个monad实例m,先通过函数g进行flatMap,然后对结果再通过函数f进行flatMap。在右边,我们创建一个匿名函数 x => g(x) flatMap f ,即对传入的参数x先执行函数g,然后在使用函数f进行flatMap,最终m使用这个匿名函数进行flatMap。两边有相同的结果。

在”for”表达式里组合法则会让你惶恐的逃离,所以我建议略过它。

M3b.for (a <- m;b <- g(a);result <- f(b)) yield result 
     ≡ 
    for(a <- m; result <- for(b < g(a); temp <- f(b)) yield temp) yield result

从这一法则,我们可以推导出函子组合法则。也就是说打破了monad组合法则,也打破了(更简单的)函子组合(functor composition)。

证明涉及了针对这个问题抛出的若干monad法则,不是给胆小的人看的。

m map g map f ≡ m map g map f   // I'm pretty sure
m map g map f ≡ m flatMap {x => unit(g(x))} flatMap {y => unit(f(y))} // by FM1, twice
m map g map f ≡ m flatMap {x => unit(g(x)) flatMap {y => unit(f(y))}} // by M3a
m map g map f ≡ m flatMap {x => unit(g(x)) map {y => f(y)}} // by FM1a
m map g map f ≡ m flatMap {x => unit(f(g(x))} // by M2c
F2. m map g map f ≡ m map {x => f(g(x))} // by FM1a

总是失败的Zeros (Total Loser Zeros)

List有Nil(表示一个空List),Option有None。Nil和None看起来有某些相似之处:它们都表示空 (emptiness)。形式上它们被称为monadic zeros(译注: 可理解为单位元

一个monad可以有多个zeros。举例来说,想象一个类似Option的monad,我们称为Result。一个Result可以是Success(value)或者Failure(msg) 两者中的一个。Failure构造器有一个String类型参数表示为什么发生失败。对Result来说,每一个不同的failure对象是一个不同的zero。

一个monad可以没有zeros。不过所有的集合monads都有zeros(空集合),其他类型的monads可以有或没有,取决于是否它们有一个遵循zero法则的”emptiness”或”failure”的概念

第一条zero法则:Identity

如果mzero是一个monadic zero那么对于任何函数f,它的意义是:

MZ1. mzero flatMap f ≡ mzero

翻译成德克萨斯话: if t’ain’t nothin’ to start with then t’ain’t gonna be nothin’ after neither.

这一法则让我们派生出另外的zero law

mzero map f ≡ mzero map f // identity
mzero map f ≡ mzero flatMap {x => unit(f(x)) // by FM1
MZ1b. mzero map f ≡ mzero // by MZ1

对一个zero使用任何函数来map,结果都是一个zero。这条法则明确zero是不一样的;unit(null)或一些其他构造方法可以构造出的“空,但不是”真空”(empty enough)。

看看为什么这样:

unit(null) map {x => "Nope, not empty enough to be a zero"} 
≡ 
unit("Nope, not empty enough to be a zero")

第二条 zero法则:M to Zero in Nothing Flat

zero identity法则的反面看起来像:

MZ2. m flatMap {x => mzero} ≡ mzero

把所有元素都替换为nothing,结果也是nothing,嗯。。。当然。这条法则确立了你对如何将zeros进行”flatten”的直觉。

第三和第四 zero法则: Plus (相加操作)

Monads有zeros,也可以有类似加法的操作。对于List,“加法”(plus)等价于":::",对于Option就是"orElse"。不管它叫什么,签名类似下面:

class M[A] {  
    ...  
    def plus(other:M[B >: A]): M[B] = ...  
}  

加法(plus)要遵循下面两个法则才有意义:任何东西与zero相加还是那个东西

MZ3. mzero plus m ≡ m
MZ4. m plus mzero ≡ m

如果两者都不是monadic zero的话,加法法则不会说 "m" + "n" 是什么。这完全交由你决定,不同的monad会有不同的实现。典型的,如果monad的串联有意义,加法(plus)就是串联的结果。否则,它会表现的像一个或(or),返回第一个non-zero值。

回顾Filtering

在前边的部分,我简单提到过filter可以看做纯粹的monadic术语。monadic zeros只是一个小诀窍(trick)。提醒,一个可过滤的monad看起来像是:

class M[A] {  
    def map[B](f: A => B):M[B] = ...  
    def flatMap[B](f: A=> M[B]): M[B] = ...  
    def filter(p: A=> Boolean): M[A] = ...  
}  

过滤方法完全可以用一条简单规则来描述:

FIL1. m filter p ≡ m flatMap {x => if(p(x)) unit(x) else mzero}

我们创建一个匿名函数,接受x返回unit(x)mzero(取决于x的断定结果)。接着用这个匿名函数对m进flatMap。这儿有几个结果:

m filter {x => true} ≡ m filter {x => true} // identity
m filter {x => true} ≡ m flatMap {x => if (true) unit(x) else mzero} // by FIL1
m filter {x => true} ≡ m flatMap {x => unit(x)} // by definition of if
FIL1a. m filter {x => true} ≡ m // by M1

所以用常量”true”过滤结果是同一对象。相反:

m filter {x => false} ≡ m filter {x => false} // identity
m filter {x => false} ≡ m flatMap {x => if (false) unit(x) else mzero} // by FIL1
m filter {x => false} ≡ m flatMap {x => mzero} // by definition of if
FIL1b. m filter {x => false} ≡ mzero // by MZ1

用常量”false”过滤结果是一个monadic zero。

副作用

贯穿这篇文章我隐含假定了没有副作用。让我们重新看第二条: 函子法则(functor law)

m map g map f ≡ m map {x => (f(g(x)) }

如果m是一个包含若干元素的List,那么左边和右边的操作的顺序将不同。在左边,对m中的每个元素都会调用g,然后再对结果中的每个元素调用f。在右边,对函数f和函数g的调用是交织的。如果f和g有副作用,比如执行IO或修改了其他变量的状态,有人将一个表达式重构为另一个,那么系统可能会有不同的表现。

注意:在定义或使用map,flatMap和filter的时候要避免副作用。

坚持foreach的副作用,要在每次定义时要给出一个警告标记,重排序可能会引起不同的行为。说到这儿,foreach的法则在哪儿?嗯,鉴于foreach没有返回任何结果,唯一可以表达的规则是:

m foreach f ≡ ()

这将意味着foreach什么也没干(译注:上面的小括号表示Unit类型实例)。从一个纯函数式意义上: 它用f函数转换m为一个空(void)结果。但是foreach意味着副作用–它是命令式结构。

第三部分的结论

直到现在,聚焦在Option和List让你用直觉感受monads,通过这篇文章最终你会看到真正让monad成为monad的是什么。结果表明monad法则与集合没有关系,它们比集合更“泛”(general)。只不过monad法则恰好对集合非常适用。

在第四部分,我将呈现一个已发育成熟的大象 monad,与集合没有任何相似,在适当的光线下它仅仅是个容器。

下面是Scala与Haskell的法则对比

Scala Haskell
FM1 m map f ≡ m flatMap {x => unit(f(x))} fmap f m ≡ m >>= \x -> return (f x)
M1 m flatMap unit ≡ m m >>= return ≡ m
M2 unit(x) flatMap f ≡ f(x) (return x) >>= f ≡ f x
M3 m flatMap g flatMap f ≡ m flatMap {x => g(x) flatMap f} (m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)
MZ1 mzero flatMap f ≡ mzero mzero >>= f ≡ mzero
MZ2 m flatMap {x => mzero} ≡ mzero m >>= (\x -> mzero) ≡ mzero
MZ3 mzero plus m ≡ m mzero ‘mplus’ m ≡ m
MZ4 m plus mzero ≡ m m ‘mplus’ mzero ≡ m
FIL1 m filter p ≡ m flatMap {x => if(p(x)) unit(x) else mzero} mfilter p m ≡ m >>= (\x -> if p x then return x else mzero)

翻译 monads-are-elephants 第二部分

原文:http://james-iry.blogspot.com/2007/10/monads-are-elephants-part-2.html

在第一部分,我通过盲人摸象的寓言介绍了Scala的monads。正常情况我们认为每个盲人所说的不会让你对大象的理解更接近一些。但我提出另一种观点,如果你听到了所有盲人对他们经历的描述,你很快会对大象建立一个令人惊叹的理解。

在第二部分,我将通过探索scala的monad相关语法糖“for-comprehaension”来更深的戳一下这个怪兽

一个简单的”For”

一个非常简单的”for”表达式,看起来像这样:

val ns = List(1, 2)  
val qs = for (n <- ns) yield n * 2  
assert (qs == List(2, 4))  

“for”可以读作 “for[each] n [in] ns yield n*2” 它看起来想一个循环,但它不是。这是我们的老朋友map的伪装。

val qs = ns map {n => n * 2}  

这儿的规则很简单

for (x <- expr) yield resultExpr  

展开为(注释1)

expr map {x => resultExpr}  

提醒,它等同于

expr flatMap {x => unit(resultExpr)}  

多一点的”For”

for(的括号)中只有一条表达式并不很有趣,让我们再加一些

val ns = List(1, 2)  
val os = List (4, 5)  
val qs = for (n <- ns; o <- os) yield n * o  
assert (qs == List (1*4, 1*5, 2*4, 2*5))  

这条”for”可以读作”for[each] n [in] ns [and each] o [in] os yield n*o” 这个形式的”for”看起来想一个嵌套循环,但它不过是map和flatMap。

val qs = ns flatMap {n => os map {o => n * o }}  

值得花一点时间理解一下。这儿是它怎么计算的:

val qs = ns flatMap {n => 
            os map { o => n * o } //1
          }

val qs = ns flatMap {n =>        //here
            List(n * 4, n * 5)  //上面的1
         }

val qs = List(1 * 4, 1 * 5, 2 * 4, 2 * 5) //上面的here

更多的表达式

让我们更进一步

val qs = for (n <- ns; o <- os; p <- ps) yield n * o * p  

这个”for”展开为

val qs = ns flatMap {n => 
            os flatMap {o =>   
                {ps map {p => n * o * p}}}}  

这个看起来与之前的那个相似。这是因为转换规则是递归的:

for(x1 <- expr1;...x <- expr) yield resultExpr  

转换为

expr1 flatMap {x1 =>   
    for(...;x <- expr) yield resultExpr  
}  

这条规则可以重复的应用(转换为flatMap),直到只剩下一条表达式,将被转换为map。这儿展示了编译器怎么展开 "val qs = for …"

1. val qs = for (n <- ns;  //第一个表达式
                o <- os;    //第二个
                p <- ps)    //第三个
            yield n * o * p


2. val qs = ns flatMap {n => //上面的第一个表达式,用flatMap翻译
                for(o <- os; 
                p <- ps) 
                yield n * o * p}

3. val qs = ns flatMap { n => 
                os flatMap {o => //第二个也用flatMap翻译
                for(p <- ps) 
                yield n * o * p}}

4. val qs = ns flatMap {n => os flatMap {o =>
                {ps map {p => n * o * p}}} //第三个(最后一个)用map翻译

命令式的”For”

“for”也有一个命令式(imperative)的版本,用于那些你只调用一个函数,并不在意副作用的情况。这种版本只用去掉yield声明。

val ns = List(1, 2)  
val os = List (4, 5)  
for (n <- ns; o <- os)  println(n * o) 

这个展开规则很像yield版本的,但用foreach替代了flatMap或map

ns foreach {n => os foreach {o => println(n * o) }}  

如果你不想使用命令式的”for”,你不需要实现foreach,但既然我们已经实现的map,foreach的实现是微不足道的。

class M[A] {  
    def map[B](f: A=> B) : M[B] = ...  
    def flatMap[B](f: A => M[B]) : M[B] = ...  
    def foreach[B](f: A=> B) : Unit = {  
        map(f)  
        ()  
    }  
}  

换句话说,foreach可以通过调用map并丢掉结果来实现。不过这么做运行时效率可能不好,所以scala允许你用自己的方式定义foreach。

带过滤的 “For”

到目前为止,monads建立在一些关键概念上。有这三个方法: map, flatMap和forEach 几乎所有的for都可实现。

Scala的”for”声明还有一个特征:”if”守护(guard)。看一个例子:

val names = List("Abe", "Beth", "Bob", "Mary")  
val bNames = for (bName <- names;  
                if bName(0) == 'B'  
            ) yield bName + " is a name starting with B"  

assert(bNames == List(
    "Beth is a name starting with B",   
    "Bob is a name starting with B"))  

“if”守护会映射为一个叫做filter的方法。Filter接受一个判定函数(一个返回结果为true或false的函数)并创建一个新的monad过滤出那些与判定不匹配的元素。上面的for语句会翻译成下面的:

val bNames = (names filter { bName => bName(0) == 'B' }) 
             .map { bName =>   
                bName + " is a name starting with B"   
             }  

首先list过滤出名字以’B’开头的,然后对过滤后的list用一个追加一段字符” is a name” 的函数来进行map

不是所有的monads可以被过滤的。用容器来比拟,filtering或许可以删除全部的元素,而有些容器是不能为空的。对于这样的monads你不需要创建filter方法。

只要你不在”for”表达式中使用”if”守护,scala就不会抱怨。关于filter我在下一部分还有更多要说的,如何用纯粹的monadic术语定义来定义,什么样的monads不能被过滤。

第二部份结论

“For”是一个使用monads的简便方式。它的语法在使用List和其它集合时特别有用。但是”for”更通用(比monad),它展开为map,flatMap,foreach和filter。因此,map和flatMap在任何monad都应该定义。如果你想要monad是命令式的,可以定义foreach方法,它的实现也很轻松。filter可以在一些monads中定义,但另一些则不能。

"m map f" 可以实现为 "m flatMap { x => unit(x) }"
"m foreach f" 可以根据map来实现,或根据 flatMap 来实现"m flatMap {x => unit(f(x));()}"。甚至 "m filter p" 也可以用 flatMap来实现(我将在下一次展示)。flatMap真是这头野兽的心脏。

记住monads是大象。我所绘画的monads目前都是强调集合特性。在第4部分,我将呈现一个不是集合的monad,只是某种抽象的容器。但在第4部分之前,第3部分需要介绍一些真正monads的属性:monadic法则。同时,这儿有个作弊抄展示 Haskell与Scala的相关性

Haskell Scala
do var1<- expn1

var2 <- expn2

expn3
for {var1 <- expn1;

var2 <- expn2;

result <- expn3

} yield result
do var1 <- expn1

var2 <- expn2

return expn3
for {var1 <- expn1;

var2 <- expn2;

} yield expn3
do var1 <- expn1 >> expn2

return expn3
for {_ <- expn1;

var1 <- expn2

} yield expn3
脚注

1) Scala语言规范里实际指定”for”扩展使用模式匹配。实际的规范扩展了我在这儿呈现的规则,允许在左边使用 “<-“来进行模式匹配。太深入钻研的话会使这篇文章模糊混乱(涉及到太多其他概念)。