本文的作者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
原文链接:http://www.aqee.net/i-want-to-fix-programming/
【编辑推荐】