为什么代码总有可能变得更短?
我曾冒着被程序员世界骂得体无完肤的风险,坚定的论断,越短的代码就是越好的代码。
不接受抬杠,不接受反驳。😄
我经常问自己,为什么代码总能变得更短?
同时,为什么把一个5行的代码编程4行基本不可能,但是把几万行的代码编程几千甚至几百行经常可以做到?
这中间的最本质的数学原理是什么?
因为代码的组合是指数级的变化的,而不是线性的。这就是我对这个问题的思索。
本文写给像羊驼,胖子一样的新程序员。
第一个维度:多维度上的概念抽象
这个思考,受到文字发展历史的启发。
最早的文字其实是和现实世界一一对应的,并没有进行抽象。也就是表形,而不是表意。
比如“羊”字,最初指的不是“🐑”这种动物,而是指自己家里的那一只活蹦乱跳的家伙。如果要记录家里的三只羊,就“羊 羊 羊”。一百只就要画 100 个 羊。
直到有一天,人们的思维进化了,开始把数量这件事情和种类这件事情分在了两个维度上,开始有了数字概念。
这个在语言和人类文明史上是非常重要的篇章,以后再说三只羊,已经不用画三个“羊”了,而只需要说 “三羊” 。三是数字维度,可以指代任何的三(三个苹果,三个桌子),羊指代任何的这个种类(一只羊,10只羊),而把这两个维度一组合,一个新的概念就产生了。
古代的中国人还不善于做这件事情,据考有上百个汉字与车的部件有关,比如 軒轅輪軸轄辕轮轴衡轭軎辖等等。只不过后来大家不用为每个不同的组件都起个不同的汉字来描述,直接用组合词,比如“车轮”,“左前轮” 等等来形容。这样代码量(假设每一个新汉字的引入就是一大坨代码)就大幅减少了。
通过引入维度,然后在维度上排列组合,我们形成了极大的生产力。
比如家里有三只“好看的羊”。如果用造字的思路,可能在羊上面加一个圆圈或者别的什么表示,然后重复三遍。但是现在只需要再借用一个通用的维度,比如“美 - 丑”这个维度,就组成了 “三美羊”。
如此往复,再复杂的概念,也可以轻松的不增加代码的方式表达,比如我家里面有“三白美胖羊”。。。。
通过切分维度,然后组合维度,可以令人惊讶的减少代码复杂度。
比如
map(function, iterable)
就表达了循环的维度,而不管循环什么对象,不管对这个对象干什么,就是代替了 for() 循环的一个模式。
再比如很多设计模式里面的,比如chain of responsibility 模式,就表达了if - elif else 的概念。
这些都是在一个维度上的高度抽象。所以好的代码是维度的组合,而不是为一个具体的功能从头设计一个汉字,并且只用一次就再也不复用了。只用一次的代码一定没有表达一个重要的维度。
这也就是大熊所提出的一个概念,就是一个好的程序员为什么比不好的程序员会有10倍的差异,就是一般的程序员可能知道100个维度(或许会中文接受过大学教育都有这么多维度),而好的程序员知道103个维度,这个好的程序员不是多了3%的能力,而是多了2的3次方 8 倍的能力。甚至如果一个维度超过2个选项的话,可能会是几十倍的差异。
第二个维度:二分法
想象一下有如下代码,我们先考虑简单的平铺直叙,没有if,for这样的控制结构的情形:
def something():
statement0( )
statement1( )
statement2( )
.......
statement63( )
这种初级程序员常写的长长的函数如何变短?如果仅仅用语法糖,或者作弊的方式可能从65行降到60行。但基本上到头了。有没有可能更短?
从纯概念的角度去思考,可以用二分法,改写成如下函数:
def something():
part_0()
part_1()
def part_0():
statement0()
......
statement31()
def part_1():
statement32()
.......
statement63()
经过第一步重构,同样的功能,从65行,暂时的增加到69行。但是不着急,继续经过4次拆分,直到每个函数里面只包含两行:
def something():
part_0()
part_1()
def part_0():
part_0_0()
part_0_1()
........
def part_0_0_0_0_0():
statement0()
statement1()
........
def part_1_1_1_1_1():
statement62()
statement63()
到这一步,我们的65行函数已经被我们扩展成了 189 行。
这就是代码在重构的时候会先变长的原因。
但之后会发生什么?如果在第二层一个函数的 32 个语句和另外函数的 32 个语句还找不到什么共同点的话,在第四层,第五层,第六层,一定能够找到相似的模式。因为计算机领域的新鲜事不多,能够有 32 个完全没有相似度的组合的可能性已经不大。一定有大量的两两相似性。但一旦发现有两个函数做一样的pattern,他们两个就可以合并,比如 part_0_1_1_0_1() 和 part_1_0_1_1_0() 等多个函数可能就可以复用了。这种复用,有经验的程序员可以一眼就看得出来。
如果发生这样的事情,在最下面两三层可能会出现代码坍塌性的减少。如果在第六层发现有三个函数可以复用,就可以减少两个函数(6行),如果在第五层发现有三个函数的功能类似可以复用,就可以减少6个函数(18行)。分支中,越上面发现函数的重用机会,减少的代码越多。
如上只是一个纯数学模型。而实际上,代码应该不是一分二,更多的是一分三或者一分五,程序员也不会分解到最后一步再重新回来重构,而是通过经验在第二层或者第三层就已经不自觉的重构了。
这也就解释了为什么一两百很难优化,而几万行代码经常可以崩塌式的的减少总量。就是因为在初期的几百或者几千行代码基本上穷举了这个项目的大多数可能看到的模式,虽然增加了代码量,但是未来的几万行代码已经很少看到新的模式,基本上都重用了最早的那几百行代码,只是不同的层次而已。
这些思考不是今天的思考,在40-70年代的程序员已经开始用函数,封装,面向对象编程,设计模式等等各种方式来实践这些理论,只是今天,深入的思考这些工具和思想对于我们手头的工作的影响的程序员并不是主流而已。