再谈 Monad

网上有很多文章讲解 Monad,但是没有一篇让我感到满意,因为大多数文章不是从 Monad 的编程界面出发,就是从纯数学定义出发,这些都不能解决我们心中的根本问题,就是 “为什么需要 Monad?” 如果这个问题不解答,那么无论定义,还是使用,都只是照猫画虎而已。不知道为什么需要虎,那画虎就是纯粹炫技。

虽然本文试图解释为什么需要 Monad,但是不得不暂时离开 Monad 先谈一些别的。我们要先从为什么需要泛函编程讲起,其实这也是一个很难理解的问题。因为这里充满了禅宗式的崇拜,对于初学者而言,这种崇拜貌似有些虚无缥缈,所以不被接受,能打动他们的依然是炫技。这可能也反过来解释了为什么网上没有令人满意的答案的原因吧。

简而言之,泛函编程相对于面向对象的那些禅宗式的思想主要围绕两个方面展开。

  1. 安全。
  2. 数学式的严谨。

我们首先要解答关于“安全”的疑问。这个问题其实在早先的一篇文章《一个有关泛型的错误考题》中曾提到。在那篇文章中我说,通用泛型是不安全的,因为它没有为运行时提供适用类型的边界指导,所以取而代之的是以 type class 为形式的特设泛型。详细内容不复述了,大家自己去参考。回到 type class,如果我们打开 Cats 的官方文档,我们会发现 Cats 的开篇第一句话就是对 type class 的解释:

Type classes

Type classes are a powerful tool used in functional programming to enable ad-hoc polymorphism, more commonly known as overloading. Where many object-oriented languages leverage subtyping for polymorphic code, functional programming tends towards a combination of parametric polymorphism (think type parameters, like Java generics) and ad-hoc polymorphism.

由此可见 type class 的重要性,实际上它也确实是整个 Cats 框架的实现基础。如果你对此有疑问,或者说你对特设泛型还存在疑问,那么无论你对 Cats 的 API 背诵的有多熟悉,都不能说明你真的了解 Cats 和泛函编程,因为这同时也是 Monad 的基础。

虽然我不喜欢重复《一个有关泛型的错误考题》中的观点,但是可能还是有必要再次借用文章中的例子来解释一下通用泛型的问题,对于这样一个泛型:

class Comp[T] (val p1:T, val p2:T) {
    def getLarger = ???
    def getSmaller = ???
}

它好提问:请问怎么从北京到达北极?这个问题对一个孩子来说,他可能很容易回答你:一路向北就可以到达。但是对一个成年人来说几乎是无解的,因为“一路向北”意味着许许多多的不确定性。就好比这个类,你可以实现对 int 类型的 getLarger,也可以实现对 String 类型的 getLarger,但是你如何实现所有类型的 getLarger 呢?所以这个类的定义是一个非常美好的设想,就像“一路向北”一样,但是对于实现而言却很残酷。所以我们需要对向北的过程中的所有问题都有答案之后,才可以放心地让程序一路向北走下去。于是,我们就不得不为问题建立限定,让无限的问题,变成可期待的有限问题。

让我们再举一个例子:假设我们要为我们的应用的输入实现一系列的 Validator 来检查用户输入的合法性。从某个角度来看和 Comparable[T] 是很像的。从实践的角度,我们会定义一个 Validator[T] interface,然后实现一系列诸如 IntValidatorStringValidator…,然后将它们保存在一个 List[Validator] 中让它们逐个对输入进行过滤从而得到安全的输入。这是面向对象编程中非常常见的方式,但是问题同样是:你如何保证在运行时需要验证的类型恰好存在?这个问题听上去貌似有点杞人忧天,因为作为一个程序员,你可能很快回答:当我们在开发这个应用的时候当然知道需要对哪些类型进行验证,必然会提供相应类型的实现。这个回答符合实际,但是缺乏换位思考。当一个程序员在开发一个应用的时候,我们通常既是业务的开发者,也是工具的开发者。比如我们知道我们将会在业务中验证哪些内容,当我们在做此思考的时候,我们是业务开发者,而当我们去实现这些验证工具的时候,我们往往忽视了此时我们已经切换到了工具开发者的角色。因为两个角色在开发过程中总是交替并行,如果不具有架构的思想,我们很难发现这两者对待同一个问题的微妙差别。这么说吧,从架构师的角度出发,我们希望程序的业务部分是松散的,而工具是严谨的,因为业务总是在变,我们无法预估未来会产生什么新需求,(比如对新的类型进行验证。)而新的业务需求要尽可能从用今天设计的工具,所以工具需要具备相当的稳定性。更糟糕的是,相对于程序的开发周期而言,它的维护周期要长的多的多,很可能远远长于原创程序员的在职时间,甚至长于一个程序员的职业生涯。可以想象,若干年以后,当一个应用发生了改变,之前的业务逻辑已不再适用而需要重构的时候,你之前的工作留下的遗产,实际上更多的是你作为工具开发者时创造的价值,作为业务开发者留下的价值很快会被被消耗掉。为什么我们总是畏惧对前任开发者留下的代码进行修改呢?因为我们无法确定新的修改,比如新添加的类型验证是否已经被支持?我们需要实现新的 Validator 吗?是否有已经存在的接口定义?或新的 Validator 与现有的Validator 能协同工作吗?……一系列的问题困扰了后来者,这占二次开发成本很大的比重。作为一个架构师,我之前说过,每一个架构师的心中都需要具有时态感,需要非常清楚代码生效的工作时态。在此再提一条:作为一个架构师,你心中需要具有角色感。你需要非常清楚自己以什么角色在构建当前的代码,这样才能让你的代码具有保留(重用)的价值。很显然,type class 让我们在重构旧代码时有章可循。通过特设泛型,编译器可以轻易发现接口规范和对应的实现,作为继任者,我们只需要满足编译器提出的要求,就能保证新的业务程序不因为缺少匹配类型而奔溃。这就是泛函编程对 安全 最基本的要求,它实际上是基于架构思想,而不是基于一般程序员仅仅着眼于眼前的开发需求。如果仅仅局限于解决眼前的开发,头疼医头,脚疼医脚,那么还是回到“一路向北”问题上:你的路径不可复制,因为你一路向北到达北极的过程中解决的问题没有为日后可循留下足够的安全依据,或即便存在依据,也无法保证能够被后来者“因循守旧”(在此为褒义理解)。而遵循泛函编程的安全思想则可以实现这一点。

似乎有点跑题,花了很长篇幅在介绍 type class 的重要性,它对于 Monad 意味着什么?还是让我们来看一下 Cats 的官方文档是怎么介绍 Functor 的:

Functor

`Functor` is a type class that abstracts over type constructors that can be `map`‘ed over. Examples of such type constructors are `List`, `Option`, and `Future`.

Functor is a type class...!写的多么清楚啊!Functor 是基于 type class 的,并且它几乎是 Scala 中所有主要的数据结构,包括 List, Option, Future 等共同遵守的基础。也就是说,在泛函编程中,Monad 为 type class 提供了一个通用模版,它是当你希望利用 type class 特性的时候应该遵循的标准。当我们理解了这一点,我觉得 Monad 作为泛函编程中的一般存在的意义已经无需多言了。

当然,如果仅仅是作为模版而存在,你可能会说只要我们遵循了 type class,那么我们可以定义自己的 trait 来达到目的,为什么依然还需要 Monad 呢?确实,Monad 基于 type class,而 type class 并非基于 Monad,所以使用 type class 并非一定要遵守 Monad 制订的模版。但是 Monad 除了作为一个通用模版,满足大部分数据结构对抽象的需求外,它还提供了一些额外的特征,这些特征也是程序所必须的,对泛函编程而言尤其是第二条,对数学式的严谨的要求。

Monad 在数学方面必须遵守几条 Laws,以结合律为例:

Composition: Mapping with `f` and then again with `g` is the same as mapping once with the composition of `f` and `g`

-   `fa.map(f).map(g) = fa.map(f.andThen(g))`

还是以 Validator 为例,很显然,对数据总体外在呈现出来的验证结果并不因为局部验证顺序的不同而不同。这给程序的执行带来的好处显而易见,不是吗?数学的严谨为程序在业务逻辑的解耦方面的作用不必多言,也提升了 Monad 之间的并行性和可结合性,让 type class 结构不仅适用于单子层面,也合用于更大尺度的计算结构上,以实现 One law manages all 的美感。并且这种理念最终也会体现在实现低成本维护和运算上。限于篇幅,不再展开细说了。

总之:Monad 是泛函编程中对 type class 提供支持的应用模版,同时,它还提供了一些额外特征,使的程序运行和维护更加低成本且安全可靠,这就是为什么我们需要在泛函编程的学习中了解并掌握 Monad 的原因。

Leave a Reply
Your email address will not be published.
*
*

BACK TO TOP