Chris Grainger:我们如何才能更好地编程?

jopen 10年前

本文作者 Chris Grainger 是开源 IDE Light Table 的开发者,之前在微软 Visual Studio 开发团队效力。本文是 Chris 根据他自己的演讲《Finding a way out》改写而成。

当开始设计 Light Table 的原型时,我并没有任何宏伟目标,只是一直在思考如何能更好地编程,还想看看做到这点有多难。直到最近,我才幡然醒悟:过去的十年努力:从Web框架,到Visual Studio,到 Light Table 的这段经历让我意识到:从一开始我就错了。事实上,我犯了一个典型的新手错误:想回答一个自己都不明白的问题。

如何更好地编程?

我只是不停地问自己:“我们怎样才能更好地编程?”但从来没有退一步,具体去想编程有什么地方不对。一直以来,通过对自己和他人的工作的观察,我得到了最完整的答案。以“Light Table”为例,如果缩短反馈循环会提升性能,虽然它的确可以,但却掩盖了一个事实:只是相同的老版本迭代。人们在痛苦的深渊中挣扎,我也是。“缩短反馈循环”并没有将我们从编写和调试代码的“深渊”中解救出来。所以,如果问题不只是反馈循环,那又是什么呢?编程的问题到底在哪?

要回答这个问题,我需要更多的数据,不仅仅从我的“后视镜”或自己的经验,而是从“现实世界”。于是我开始问大家两个问题:“什么是编程?它的问题到底在哪?”我问过从未编程的普通人,正在上班的白领,同行业中最好的工程师,等等。他们的答案让我吃惊不已。

什么是编程?

这个问题的答案真是令人沮丧。问了这么多人,居然没有一个人说“编程是用来解决问题的。”相反,大家更多的觉得它只是用半干的胶水把东西粘起来盛水,并祈祷在被新东西取代之前还可以继续盛水。我听别人说过无数遍,说我们只是水管工和胶水工厂。还有人说”编程主要解决自身不足“,这让我特别难过,因为它是众多答案中唯一接近正解的答案:我们把东西放到一起来解决某个问题,而“这个问题”是让程序员沮丧的源头之一。

编程本来是用来解决问题的,却变成了解决问题的问题(一个工程师朋友喜欢称之为“自舔冰淇淋”)。“胶水工厂”肯定不是我想要的答案,所以我一直在想一些可以行得通、说得过去的答案。到目前为止,我最喜欢的一个是:programming is our way of encoding thought such that the computer can help us with it. 编程是把我们思想转成代码,并让计算机可以帮助我们实现它的方式。当一天结束的时候,我们试图做的是构建一个新东西,并能够实现它——编程恰好是实现它的最好方式而已。

 

编程的问题在哪呢?

这个问题的答案也是我不想再问人(约400人)的原因。他们往往分为两类:他们要么随大流,并没有提供任何有用的信息,要么非常有个性的回答(当得到这些“死锁”时,我相当无语)。但总体而言,对于“编程的问题在哪?”这个问题的数百条答案,我提炼出了三类答案:不可察,非直达 和不简单。(unobservable, indirect, and incidentally complex)

编程是不可察

最近由于Bret和“Light Table”的开发工作,这点似乎是我们最为关注的。我们不能看到我们的程序是如何执行的。我们不能看到对程序的改变是如何影响项目。我们不能看到我们的程序是如何连接在一起。这基本上意味着我们观察不到任何东西。可观测性可以看做是一个一步一步查看状态的调试器,它强迫时间停止,并观察在某个时间瞬间的状态。但事实是,程序在某个时间瞬间基本上不会出错;大多数错误都是在一段时间内发生。就因为这一点,我们最好写一个print语句。但这十分荒谬。我们需要的是观察整个计划执行的过程,后进,后退,甚至未来——而不仅仅只是当我们的断点命中时。比这更可悲的是,我们似乎已经接受了不可观察性为一个既定的事实。例如下面这行代码:

person.walk();

它有什么作用?面向对象的封装的定义就是不可观察性。我不知道person.walk()有什么用。它可能做一些事情,像设置isWalking为true,但它也可以设置ateCarrots为true,它也可以决定我是否因疲惫而昏厥——我不知道,也没有办法知道。就好像,我们在一片漆黑中无所顾忌地扔飞镖并祈祷我们至少能射中靶心。不管你是刚刚起步或已经写了数以百万计的漂亮的代码行的程序员,根本无法看到我们的程序怎么运行,这是一个严重的问题。

编程是非直达的

写一个程序是一个容易出错的翻译工作。即使是数学——编程语言因其而生,都被翻译成这样了:

#include <algorithm>  #include <iostream>  #include <iterator>  #include <cmath>  #include <vector>  #include <iterator>  #include <numeric>    template <typename Iterator>  double standard_dev( Iterator begin , Iterator end ) {     double mean = std::accumulate( begin , end , 0 ) / std::distance( begin , end ) ;     std::vector<double> squares ;     for( Iterator vdi = begin ; vdi != end ; vdi++ )        squares.push_back( std::pow( *vdi - mean , 2 ) ) ;     return std::sqrt( std::accumulate( squares.begin( ) , squares.end( ) , 0 ) / squares.size( ) ) ;  }    int main( ) {     double demoset[] = { 2 , 4 , 4 , 4 , 5 , 5 , 7 , 9 } ;     int demosize = sizeof demoset / sizeof *demoset ;     std::cout << "The standard deviation of\n" ;     std::copy( demoset , demoset + demosize , std::ostream_iterator<double>( std::cout, " " ) ) ;     std::cout << "\nis " << standard_dev( demoset , demoset + demosize ) << " !\n" ;     return 0 ;  }

而我们为什么不能直接看到:
d1.png

这个例子,还算是翻译的比较清晰了。那要是代码不是这么清晰,又会发生什么呢?我们能得到就只有符号了。我们从来没有看到或与真实的东西交互,他们只是象征性的表示交互。而在某些情况下,符号固然重要和强大,它们不必是不透明:

cards[0][12];

是啊,打牌的时候,当我拿到卡[0] [12],我会很开心。我们正在编写一个纸牌游戏,纸牌有自己的名称(黑桃A),为什么我们不能直接存储这个呢?

d2.png

翻译很难,而且使用的符号又容易出错,尤其是当与其他元件的上层操作耦合时。这种间接性,导致其不能代表有意义的东西,如果直接使用它们,又会让我们抓狂。大量编程的错误仅仅是翻译问题。虽然我们的脑子里有解决方案,但在试图把它翻译成代码的时候,我们要不是忘了什么东西就是犯些小错误。所以,我们必须摆脱翻译。当我们设计UI,我们应该做一个可视化编辑器。当我们研究数学,我们手边应该有类似的Mathematica的工具。我们应该用最自然的方式展示他们,而不是混淆不清的方式。

编程是复杂的

在编程中通常带有许多的复杂性事件,你得是要做一堆并不与你要解决的问题直接相关的工作。试着计算即使得到最简单的运行结果所需的时间,至少要花一个星期的时间来搭建一个开发环境。这些例子都是一个系统中不必要的复杂性,但事实是这种偶然的担心普遍存在的整个编写软件的全过程。最糟糕的是,我们现在是在逻辑层用代码做时间管理。大多数人可能会反驳,我们只要立即跳转到并发和并行就行了啊,但实际问题不是这么简单的。每当我们添加一个事件处理程序或创建一个回调,我们就是在做时间管理。

考虑到交互模式越来越复杂,我们程序面临多内核增加的变化(随之而来的并发问题),很快我们得知回调是解决该问题的糟糕方案。每次我们想要自己手动管理事物的时候,我们都可以想出一个实现自动管理的方案。我们曾经手动处理二进制,后来我们创建了Fortran语言。我们曾经手动管理内存,后来我们创建了垃圾收集器。我们曾经管理数据的限制,后来我们有了类型系统。我认为,下一步优化代码的复杂性的重大举措将来自于实现自动时间管理,这将对我们清晰地表达意图的能力有重大的影响。

试图列举编程中所有复杂性的例子将会令人感到无望,但现在是时候要解决这些问题了。我们应该专注于解决问题——而不是解决问题的问题。这样我们就可以在一天之内从一无所有到生成解决方案,而不是需要几周或几个月的时间。

追求局部最优(Chasing local maxima

如果你回顾在过去50年主流技术上的进步,他们虽然很大程度上提高了我们的效率,却没有真正改变程序的行为。之所以我在文章的开始暗示:一切都是被动的,因为我们往往只在战术层面上的修正。事实上,几乎每一个我们取得的进步已经完美的解决了以上三类问题。我们让事情变得更好,我们不断接近局部最优,因为我们认为这些东西能以某种独立的方式解决。我听过最好的比喻是茶杯堆叠效应,每次我们修复某件事情,就是在茶杯堆上继续放茶杯,但最终我们只是在抽象多层次的工作,而该塔也开始倾斜并快要倒塌。我们必须停止单独思考这些问题,而是要想怎样才能同时解决他们。

有一天,我突然开窍了:编程其实是一切的对立的统一。晦涩的语言,神秘的错误,缺乏(或大多是零散的)文档——就像坐在某个真人秀的现场(幕后还会传来阵阵笑声)故意为难你。在一定程度上,这是自虐,但我们这样做,因为它给了我们提供了一个难得的机会塑造世界。通过认真思考编程的定义和它的问题所在,我们可以找到消除多余内容的框架的机会。但为了做到这一点,我们不能只考虑我们现在拥有的。另一个抽象层是不够的。相反,我们必须从根本的层面解决问题。不要更多的茶杯,不要更多的妥协。

编程之人

跟非程序员的聊天,他们会提供非常不同的看法。事实是,在我看来,他们大多也是程序员。他们只是不写“代码”而已。如果您使用Excel,你就是在编程——你要电脑根据你写的一个模块为你工作。 Excel是一个特别有趣的例子,这让它取得为人们能够解决问题的巨大成功。这也恰好满足我们的三个基本条件,并一些证据表明,它具有强大的解决问题的能力和“平易近人”的开发环境。

Excel是可观察的,因为它没有任何隐藏的状态,所有的值你的看到并操作。它也是直接的。你可以在表格中直接更改数值,拖放东西,选择性地计算等等。它设法回避了很多复杂性问题;电子表格是永久有效的,它没有安装程序,甚至没有的正在运行的概念。Excel通过权衡权限实现了这些。有很多事情是无法表达,因为放置在编程模型的约束条件非常好(或全部)。一个有趣的问题是,是否以类似的方式就可以解决我们的问题,但缓解了一些限制和保留更多的权力。

我们越多探讨过这个问题,就越会意识到,通过修复了这些问题我们在编程更加普遍的路上已经走了很远了。因此,如果这个有趣的问题的答案是肯定的,那大约有十亿人可以拥有现代超能力。试想一下,如果每个人都用电脑做到80%如今的程序员的工作。这样对什么的影响会最大?我不知道,但我认为它这将是我们人类作为一个集体的根本性转变,而且肯定是一件非常棒的事情。从长远来看,我相信操纵电脑将成为一项基本技能,但不像大多数的“编程是识字”(programming is literacy!)运动,我认为这将与能否写出&#8217; if &#8216;语句无关。未来编程将会是大多数人都可以做到的事。任何以我们现在这样做事的方式,是注定要失败的。事实上“受虐狂”一点也不受欢迎。

文化差异

我的问题还没有解决的另外一个原因是:编程带来的社会问题,最近也得到了越来越多的关注。有许多方面对这个问题从编程已有的固有印象,到我们社区互动的方式,已经上升到了偏见和成见。我觉得编程最有趣的地方之一是它有可能引起的文化的重新定位。如果编程本身并不对立,那会变成什么样呢?如果只需要粗略地计划就能编程,又会是什么样?如果编程从一开始就是思路清晰的?这样的编程,与我们如今的编程没有一点共同之处。因为解决编程带来的文化问题才是真正地改变世界。

好,现在该怎么办?

我们找到一个解决问题的基础!没问题吧?在“Strange Loop”的演讲中,我展示了我们一直都在做的Aurora非常早期的原型。这里,我带来了新的进展。虽然我们的策略已经有了显著改变,其背后的概念仍然是相同——我们想要找更好的东西,不只是针对程序员或非程序员,而是完全消除这种区别。我们最近在这个方向做了些实实在在的进展,当稳定之后,我们将分享更多。但有一点我想说的是,大家一定要眼见为实。

原文链接: Chris Granger   翻译: 伯乐在线 - Stellar
译文链接: http://blog.jobbole.com/65896/