这个系列是记录一下学习函数式编程的心得,在网上搜索相关资源的时候,发现这玩意居然还是一门课,大概有10来个小时,所以一篇文章肯定是搞不定的。想着就搞一个系列,但是更新不会稳定,随缘吧。
忘记OOP
在武当山上,方东白化名阿大,手持倚天剑向张三丰挑战。
张三丰此时已经身受重伤,无力应战。张三丰将自己新创的太极剑法传给了张无忌。
张三丰传剑的过程,颇让人感到诧异。他不仅当众传剑,让方东白看得清清楚楚,而且招数慢吞吞,软绵绵,竟让众人以为张三丰有意放慢了招数,好让张无忌瞧得明白。张三丰共使了两遍剑法,第二次所使,和第一次使的竟然没一招相同。最后张三丰让张无忌将剑招忘得干干净净,才叫他与方东白比试。
张三丰传剑大违常理,这让周颠等人很是担心。没想到张无忌一出手竟大奏奇效,彻底击败了方东白,并以木剑斩下他的一条手臂。
忘记剑招是学习太极剑的关键,同理忘记OOP是学习函数式编程的关键。
我们先来看一个程序的组成:
我们可以将程序分为2个部分:
上面的 classes,methods,inheritance 等就是OOP的重要组成部分。
函数式编程就相当于是对programs的组成进行一个重构。
重构应该绝大部分人都经历过了,假设我们有一个业务,刚开始每个模块都工作的非常好:
随着需求的迭代,我们需要新增一些模块,删除一些模块:
甚至开始在模块之间添加依赖关系:
现在,我们的程序还是能够工作,但是一言难尽,特别是修bug,可能涉及到很多的地方。这个时候应该怎么做呢?如果你非常的幸运,你/你的领导会想,维护这坨屎,还不如搞个新的:
但是,真的需要从0开始吗?没有人喜欢从0写一套新的东西,一般我们都会审视旧的模块,有的写的好,有的写的差,我们会将好的模块留下来,坏的丢弃。
所以重构的核心,在于重新组织现有的好的模块,然后添加新的部分来让这些模块好好工作。比如说,添加一个总线:
现在回到我们套路的问题,为啥我们需要忘记OOP,其中的一点就是OOP有点太过了(当然对于大多数人来说都可能无法体会),就拿泛型的类型擦除来说,就很奇葩,类似的还有C++的友元函数,规则复杂的一逼。这是一种预兆,OOP开始束缚我们的脚步了,它变的有点混乱了。
我们就需要像重构业务一样,重构我们的程序的组成部分:
什么是 f(x)
f(x)
是从数学里面借过来的一个概念。比如:f(x) = x * 2
,这个函数就是将任意的输入 x,变成输出 x * 2。
在函数里面,一个重要的特征就是,我们放一个东西进去,然后它给我们一个结果,没有其他任何的多余动作。
假设我们将函数看成一个黑盒子,那么它就是这样的:
这个黑盒子在工作的时候,不会对外界产生任何影响,也就是我们常说的没有副作用(No Side Effects)。
看一个例子:
ini
x = ['a', 'b', 'c']
y = a(x)
x = ?
在这个例子中,x在函数执行后是多少呢?当然还是x原来的值,它不会变化。函数没有副作用不仅仅是不会影响外界,而且连参数都不会改变。
再看这个:
x还是原来的值!!!
这就带来一个问题,比如在 java 里面,x 是一个数组:
我们的函数需要改变第2个位置的值,假设我们的函数如下:
那么,这就改变了参数的值,与函数的定义违背了。解决办法就是将参数的值 copy 一份:
那么这又带来一个问题,我们可能需要拷贝很多很多份数据:
解决办法是我们需要设计新的数据结构,比如将数组变成链表,举个例子:
当 n 变化的时候,我们只需要拷贝3个节点,其余的可以复用。
说的有点远了,我们回到函数编程上来。有了函数和不变性数据结构,我们就创建了一个纯净且完美的世界,我们的数据不怕被篡改,也没有任何副作用。但是又出问题了,没有副作用才是最大的问题。
什么是副作用?写文件,输出到控制台,操作数据库等等都是副作用,用户是把副作用当作程序的核心,用户不会关心我们的代码,他们需要的就是副作用。
一个没有副作用的程序只是一个空中楼阁,很美好很纯净,但是啥都不能做。这就有问题了,所以我们需要一个桥梁,链接我们的美好世界到外部的纷争世界:
桥梁
实现通往外部的桥梁有很多问题需要解决,其中之一在于,如何改变程序中的状态?举个例子:以一个website,它记录了访问次数,这个功能该如何使用函数实现?
假设我们有这样的一个结构,Atoms:
Atoms 是一个可变状态的容器。那么这个时候就有人要问了:这不就是一个变量吗?有啥区别?
它与变量的不同之处在于更新自身的方式不一样。
因为Atoms 是一个桥梁,所以,我们只需要给他一个函数就ok了:
你可能觉得这没有什么大不了的,我搞个变量,应用这个函数不也一样。那么我们再深入一下,我们有两个线程来更新这个 Atoms:
这个时候就需要处理同步问题,按照我们一般的写法,甚至需要给 f(x) g(x) 进行加锁。但是函数式写法不需要,一个值得注意的点是函数是没有副作用的,而且输入不变的情况下输出不变,所以我们重复执行函数是没有任何问题的。
意识到这点,我们就可以将同步的处理限制在 Atoms 里面。假设当 f(x) g(x) 同时更新,g(x) 先执行了,那么 f(x) 再继续更新的时候,Atoms 会注意到这个行为,它就会让 f(x) 获取到最新的值,重复执行一次即可。
结尾
刚开始解除函数式编程的时候,写起代码来可能会束手束脚,就感觉自己带了一个手铐一样,但是当你有了一定的积累之后,就会发现手铐变成了自行车车把,虽然你必须将手放在车把上面,但是你达到目的地的时间会缩短。
其实函数式编程与OOP,我个人没啥偏向,看起来它们是背道而驰:
但是kotlin刚好两者都有,或许你自己可以在其中达到一个平衡。
希望通过上面的讲解,能够让你对函数式编程有一定的理解。它的3个重要部分:
- 函数
- 不变性
- 桥梁