理解函数式编程语言中的组合--前言(一)
函数式编程思想可以用一句话总结,即:可组合的类型+可组合的函数,我在《使用函数式语言做领域建模》一文描述了如何使用可组合的类型进行领域建模。这篇文章就是用来说明后半部分,即--理解可组合的函数。我假设读者已经对“Higher order function”, “Currying“, ”Immutable“等基本概念有所了解,并拥有基础的TypeScript知识即可。
这一切还得从抽象说起。
抽象的重要性
人类能够解决复杂事物的一个重要方法就是抽象,代码也一样。有了抽象,你的代码才不会变成流水账,实际上那些让人读起来赏心悦目的好代码,一定拥有良好的抽象。抽象可以让你避免陷入细节,通过几个重要的类名或者接口,让别人快速识别你的设计。
当然,这一切的前提是,阅读代码的人也拥有类似的抽象思想,他才能够心领神会你的意图。如果你在读代码的时候,自己是怎么想的,正好代码就是这样设计的,你才会觉得代码可读性太强了。
怎么样大家才能设计出一致的抽象呢?
《设计模式》就是这样一本把常见问题总结成一些列模式的书籍。换句话说,《设计模式》提供了常见问题的抽象方式并提供了相关的术语。
当然你的设计好不好,还取决于你是否正确识别到了问题的本质,并恰如其分的实现了某个已知的设计模式。如果阅读代码的人拥有跟你类似的抽象知识,他就能够迅速明白你的设计和意图。
函数式编程中的抽象
如果说《设计模式》总结了OO思想中的常见抽象方式,那么函数式编程语言也拥有自己的抽象方式。
看下面的这两行数学运算:
1 + 2 = 3
这行数学运算非常简单,以至于靠直觉就能判断他的真确性。这行代码可以描述为:两个“数字”通过“相加”任然为“数字”。
如果我们把"数字“泛化,将"相加”推广开来,就可以用在其他事物身上,例如:
"a" + "b" = "ab"
对字符串也是适用的,两个字符串通过一个运算符合并为一个新的字符串。没错,这就是组合的基础。
函数式编程中的抽象叫做《范畴轮》Category theory,是一门研究如何组合事物的数学科学。其中,“事物”被称为object, 但这个object并不是OO中的那个对象,而事物之间的转化或者映射被称为morphisms,翻译为中文叫射态,对应到编程语言中,就是函数。《范畴论》在函数式编程语言中的地位,可以类比《设计模式》在OO中的地位。
为什么需要《范畴论》
这要从函数的组合开始说起,我们知道函数式编程语言没有OO语言中的那些概念,例如类,继承,依赖注入(有的语言有FP和OO两种范式, 例如Scala, F#等,请不要纠结)。只靠函数,为大型的工程化实践带来了挑战。 玩过乐高的同学都知道,乐高的零件都是拥有独立功能的小部件,然而,却有人用它拼出了汽车,飞机甚至是航空母舰,这充分说明了组合的力量。
那么函数是如何组合的呢?看下面的代码:
const add = (a: number, b: number) => a + b
const sub1 = (a: number) => a - 1
声明两个函数add和sub1, 像lodash或fp-ts等库都会提供一个叫flow的函数,flow的作用就是把若干个函数组合起来:
const addThenSub1 = flow(add, sub1)
expect(addThenSub1(1, 2)).toBe(2)
此时addThenSub1变成了一个新的函数,最后再将参数作用在上面就可以得到结果。flow理论上可以支持任何数量的函数组合。然而,flow有个致命的问题,他要求上一个函数的返回值类型跟下一个函数的输入类型一致,例如,上面例子中的add如果返回string, 那么连接就失败了。另外对sub1的参数数量也有要求,即除了第一个函数add, 后面的函数只能接受一个参数。
其实这两条限制就为函数的组合判了死刑,即:
虽然函数是可以组合的,但并不是所有的函数都能任意组合。
《范畴论》就是这样一门数学科学,他帮你抽象出了事物的转化或组合模式,你只要按照他总结的模式设计,就能将函数组合起来。