本文的作者Jon Beltran是一个西班牙程序员,作家,企业家,大学时辍学专职做游戏开发,他目前主要经营Symnum Systems公司,开发 ViEmu
和 Codekana 这两个开发工具。
软件编程出问题了。出大问题了。如今的这种编程方式让人如此不堪忍受,以至于让人想吐。数年来我一直在说我痛恨编程。过去的20年,我一直是个全职的软件开发者,目前也是,我没后悔过,我仍然热爱着我可以用编程来做的事情。可仍然,我痛恨编程。
现在的编码方式是一种让大脑自残的方式。编写过程中的每一步,你都可能使程序崩溃——耗尽了内存,访问了错误的指针或引用,或进入了死循环。毫无疑问,编程给人的感觉就像赤脚走在到处是碎玻璃的地板上。一小寸误差的落脚距离,喀嚓,你就损失了半个脚趾头。
这种编程方式的每一步,在每一个语句里,每一行代码里,函数调用或过程里,如果你想写出能用的代码,你必须要考虑整个程序中所有的不同的、可能的状 态。这些状态是不可见的,你不可能给它们明确的定义。事实就是这样。一直是这样。包括现存的所有的语言。这就是为什么100%的代码测试覆盖率也不能保证 代码里没有bug,永远也不可能。这也是为什么差程序员不能变好的原因:根本没有一个结构化的方式让他们考虑到所有这些可能的情况。
(顺便提一下,当遇到了多线程程序时,这种情况会恶化1000倍——不是变得更好,而是更坏。)
问题的原因就在于,代码被写出来的基本方式就是错误的。完全是错误的。你写出了一行行的指令,一步一步,看起来你把程序驱动到了一个想要的状态。但每一步都是相互独立的,只有编译器/解释器能独自的理解它们,你基本上是很容易把事情做错,而不是做对。
函数式编程也许是一种解决方案,我思考了很长时间,做了认真的研究。Lisp,Haskell。Lambda计算。函数式的编程方式确实给常规的命 令式或面向对象的编程方法带来了不少改进。但这仍不能根本解决问题。它仍然是由很多无联系的简单步骤组成,痛苦的计算出输出结果。
这种编码方式关键是什么地方出了问题?关键地方就在于,你不是在表达你想要什么。你表达的是需要采用什么步骤。试想一下,你让朋友从冰箱里拿出一瓶啤酒,一步一步来,每一步都如机器人般的刻板,每一步都不关系到下一步做什么。这是在折磨一个人。极有可能造成灾难性的失败。这跟现在的编程方式是完全一样的。
程序库(lib)能带来有用的帮助,但它们只是为应付上层特定需求的快捷方式。它们解决不了真正的问题。
最近出现了一篇非常有趣的John Carmack所写的文章,讲的是静态代码检查,他引用了一条说的非常正确的微博,是Dave Revell写的关于代码检查的:
“我越用静态代码分析来检查代码,我越发现计算机的强大之处。”
一种观点
那么,应该如何编程?让我们来举个简单的例子:排序。假设你有一个输入序列,让我们称它,呃哼,输入值。假设它有几个元素。现在我们要计算出一个新的序列,称它为输出值,里面要包含有相同的元素,但元素是经过升序排序过的。我们如何去做?
传统的方法有冒泡排序,快速排序,shell排序,插入排序,等。这些都是能够让我们对一个序列进行排序的方法。例如,冒泡排序:
def bubble_sort( input, output ): output = input # start with the unsorted list swapped = True while swapped: swapped = False for i = 1 to length(output) - 1: if output[i+1] > output[i]: swap( output[i+1], output[i] ) swapped = True
非常的直接。但如果打算去写出这种排序的代码,你仍然会犯错误!你可能会在交换两个元素时忘记了把“swapped”参数设置成true,或者更典型的,你可能在循环计数时犯下忘记减一的错误。
这就是我为什么要说这种编程方式有问题的原因:排序是一种很简单的可以掌握和描述清楚的概念,可是,用代码去实现它却是复杂的,充满了陷阱,随时造成程序的崩溃,或输出错误的结果。一件难事!
有人可能会写出一种函数式的上面的算法,但相似之处会是非常明显的:没有副作用,可仍然包含完成这个任务所需的很多步骤。递归也许会比迭代更优雅(呃哼),但它并不是本质上更好。
那么,对于一个排序操作,它真正的代码应该是什么样的呢?这多年来,我慢慢总结出,它应该是一种类似这样的东西(请原谅,这些是只是一些伪代码,一种不存在的编程方式):
def sort(input[]) = output[]: one_to_one_equal(input, output) foreach i, j in 1..len(output): where i < j: output[i] <= output[j]
让我对它做一些解释:这第一行对sort的定义是说,在输出序列和输入序列之间已经存在一种1对1的“关系”。我们下文中会介绍在one_to_one_equal的定义中如何实现这个。这样一来输入和输出序列中确保了相同的元素。它在空间上定义出来可能的答案。
第二,关键点,这下面的行指明,对于输出序列中的每一对元素,当第一个的索引低于第二个的索引时,它的值也是较小或相等。这本质上就指明了输出序列上排序过了。它定义了解决方案中的一种可能的答案。
这是如此的简单。排序函数只是说明排序的结果,而不是如何做。它描述了输出数据,以及相关的输入数据的特征,它把如何能达到这个结果的任务交给了编译器。
无庸置疑,这存在两个关键问题:
- 首先,编译器如何能完成这个任务?真的有这种可能吗?在将来的文章里,我将会告诉你这是可能的,真的可能,编译器甚至能知道采用什么样的算法来获得这样的结果。
- 第二个问题是,如果把它应用到更复杂的情况中?我还是能向你展示,这种方式完全可以应用到任何的所有的编程和计算任务中,它只是一种更简单,更有效,更能避免错误的编程方式!
我曾经想不公开这种技术,将来成立一个公司来实现这种思想,但多种环境因素使我重新思考这个计划。现在我向大家分享了我的认识,想看看事情会如何发展。请关注本系列中的下几篇文章。
尾注
在本系列的后续文章中我会做深入讲解,这里只稍微提一点。这个one_to_one_equal函数在这种理想化的语言中将会是一个“标准库函数”,它多少看起来应该像这个样子,像下面这个基本逻辑:
def one_to_one_equal(output[], input[]) = c: c = relations(input[i], output[j]) foreach x = input[i]: len(c(x,)) = 1 foreach x = output[i]: len(c(,x)) = 1 foreach x(a,b)=c[i]: a == b
让我来解释一下:这第一行的定义是说,在输入和输出序列中的元素间有一个1对1的“关系”集合。
这第二和第三行指明,对于每一个输入和输出序列的元素,在集合“c”中都有一个单一的关系从属于它们,确保了它们的关系是一对一的。这最后的一行指明每个关系上的两个元素都是相等的,确保这两个序列是相同的,只是排序过。
[本文英文原文链接:I want to fix programming ]