作为程序员,如何培养审美情感
我们大概都阅读过或听说过类似于编程之美或架构之美或数学之美的著作,那么,代码到底美不美呢?如果是美的,是怎样的美呢?又该怎样理解和欣赏这种美呢?不妨把问题向美学或艺术的稍深层次的内涵稍稍推进一步,姑且把程序员视为艺术家,那么,代码有可能作为他们审美情感 (Aesthetic Emotion) 的对象吗?程序员可以像艺术家那样工作吗?
如果这一切都是否定的,那么,Donald E. Knuth 的煌煌巨著又何以称为计算机程序设计艺术 (The Art of Computer Programming) 呢?这些问题涉及到艺术和技术的关联,都是很大的问题,以我对艺术的粗浅认识以及对程序的有限了解,既不敢也不能给出确定的答案,这里只是谈谈自己阅读的感受和体会。
从产出的作品和结果的表面形式来看,代码和艺术作品的相似性也许无从谈起。从创作或创造的过程来看,程序和艺术,程序员和艺术家的差别似乎并没有想象中那么大。有关艺术的最经典的和最古典的理论莫过于 Clive Bell 在他的著作《艺术》(Art) 中提出的假说,即可视艺术如绘画的本质属性是有意味的形式 (Significant Form)。所谓形式即颜色和线条的组合与关系,之所以有意味,是因为在作品的形式中注入了艺术家的情感。对艺术作品敏感的人,正是因为发现了艺术作品中的有意味的形式,激发了和艺术家共鸣的情感,才被深深打动。这一假说被誉为最令人满意的现代艺术理论,所以,我们假设它是合理的,真实的,可信的。
那么,按照这种理论,在构思作品时,艺术家是怎样获得要表达的情感呢?有意味形式的灵感火花又是从哪里来的呢?比较客观的回答是八仙过海,各显神通,不能一概而论。但是不可否认,确实有这样的艺术家,他们基于客观的物质对象或想象的虚拟对象的形式的理解和体验,在电光石火的刹那灵光乍现,茅塞顿开。他们的诀窍和天赋在于观察物质世界和构想虚拟世界时,统统地把所有的对象看作纯粹的形式,即线条,颜色,和形状,以及它们的关系,而完全忽略对象的功利性的标签。
比如房间中有一把椅子,普通人首先给这东西贴上椅子的标签,然后想到的是它的世俗的功用,可以坐在上面读书,吃饭,聊天。当然,调皮的熊孩子们对椅子使用的想象力就更不用说了。若考虑地更远一点,也许还会联想到座次名位,如水浒中的头把交椅,聚义厅,忠义堂,替天行道,朝廷招安等等。而富于天赋的艺术家仅仅忘情于椅子的纯粹的形式, 而椅子是什么,叫什么和用做什么根本不在于艺术家的思维光谱中。
从美学角度上看,只有纯粹的形式才能揭示出物质的本质属性和终极现实 (Ultimate Reality),用一个形而上学的术语,即物自体 (The Thing in Itself )。物质的本质属性和终极现实不就是哲学家孜孜不倦地追求和探索的终极目标和终极真理吗?这岂止是有意味啊,物质对象经由纯粹的形式升华到形而上学的终极真理,恐怕世上再没有比这更伟大更有意义的事业了。无论是艺术家还是鉴赏艺术的观众,伟大的艺术作品象纽带一样牵引着世俗的我们去体验美学的出世的终极真理和终极现实,心中的情感和快感像火山一样激烈澎湃,情不自禁,难以自己。
回过头来,看看程序员是怎样观察物质世界的。从面向对象的角度分析,我们把给定的问题域或应用域内的所有的一切都看作对象和对象之间的关系。对象中包括成员变量和方法,继承、组合和关联是对象之间最普遍的关系。对象和对象的关系也是面向对象的编程语言所能处理的全部。所以,需求可以千变万化,万变不离其宗,把需求描述的问题域或应用域抽象成对象和对象的关系,是以不变应万变的唯一的解决之道。因此,反过来讲,面向对象的方法为程序员提供了一种观察世界,或更具体地讲,描述应用域和问题域的抽象框架。
那么,程序员是怎样做到把问题域或应用域抽象为对象和对象的关系的呢?较为客观的回答是八仙过海,各显神通,不能一概而论。并没有确定的,机械的,可操作的流程把需求转换成对象和对象的关系。否则,一旦这一转换过程掉入邱奇图灵论题可计算的范畴,程序员的工作马上就可以被机器取而代之了。某些程序员在某些项目上做的好一点,某些程序员在某些项目上做的差一点,还有些所谓的程序员在所有项目上都做的一团糟。这种不确定性不就是程序创作的艺术性吗?
在面向对象的课程里确实提到了一种简单的方法:假设给定一段描述需求的文本,把其中的名词当作备选的对象,把其中的动词当作对象包含的方法,把形容词当作对象可能的属性即成员变量。事实证明,没有程序员可以严格按照这种方法得到应用域或问题域的抽象对象模型。因为,需求的文本常常是模糊的,似是而非的。你若当真,那就傻了。
可能有人会说,对象建模更像是软件架构师的工作,关程序员何事?从抽象的角度看,程序员和架构师在解决问题时毫无疑问都会用到同样的抽象思维方法,即他们都是具有抽象思维能力的软件工程师,若再做进一步的抽象,大概没有人反对,在软件这个行业,研发一线的程序员 (开发工程师)、测试工程师、架构师和项目经理都是会用脑子解决问题的人。如果你像程序员一样工作,你就是程序员;如果你像架构师一样工作,你就是架构师;软件工程师角色的界限是模糊的,不能也没必要分的清清楚楚。我不知道是否有完全不会写代码的架构师,但很少有程序员承认自己不懂架构和设计。
因此,到目前为止,我们可以发现,艺术家和程序员的思维方式起码有一点是相同的,即抽象。艺术家把物质世界和想象的虚拟世界抽象到形而上学的哲学高度,因此,艺术是普适的,永恒的。而程序员只要把问题域和应用域抽象为对象和对象的关系,进而转换为代码。抽象的高度决定了抽象的难度,优秀的程序员虽是稀缺资源,但并不鲜见,而伟大的艺术家,绝对都是不世出的天才。这就不难解释:虽然计算机击败了国际象棋和围棋的人类顶尖高手,引起了人工智能可能代替或超越人类的忧虑;但人工智能在艺术领域却毫无作为,最多就是拙笨的模仿而已。
其实,就算是下棋,计算机使用的模型和人类使用的模型也是完全不一样的。当前布局到分出胜负的终局经过成千上万个中间布局存在无数条路径,组成一棵庞大的树,终局是树的叶子节点。因此可能的路径的数目相对于路径的长度是指数级的。计算机判断当前布局的优劣的依据是该布局导致最终胜局的概率,可以简单地看作是当前布局到胜局的路径的数目和到所有终局的路径的数目的比例。
我猜,AlphaGo 的深度学习算法可以多快好省的得到每个棋局的更准确的取胜的概率。不管怎样,在下棋方面,计算机比人类高明强大的地方是海量的存储和快速的查找。而人类下棋使用的是更高层次的模型,我对围棋一窍不通,用 google 搜索了一下,围棋表示下法的术语至少有几十个如挡、并、顶、爬、冲、打劫、点眼、鬼手等等,没有一个和数字相关。
我们完全不必为人类败于计算机而作杞人之忧,这和我们跑不过汽车飞不过飞机跳不过袋鼠本质上还是一个道理,计算机还没有聪明到像人类那样下棋。人类若像计算机那样下棋大概会是这样:对于当前的布局,符合规则的落子有 168 种,其中第 88 种下在“平位二八路”导致的新的布局的取胜概率是 18.68%,大于其他 167 种下法,因此,采用第 88 种下法。这样的基于冷冰冰的数字的弈棋,没有任何不确定性,比拼的只是记忆力,看谁记得多,记得准,还有什么乐趣可言呢?
悟性和灵感这些只有人类才有的智慧,另外,还有不可或缺的好运气。不乏这样的艺术作品,他们的抽象的形式到了太高的境界,远远超越了时代的理解能力,经过了漫长的岁月世人才最终认识到他们的价值。而此时,创造他们的艺术家早已在穷困潦倒中郁郁地作了古人,比如梵高。
幸运的是,程序员不是什么“圣贤”,大可不必有这样的担忧。程序员在做软件的设计和架构时,建立面向对象的抽象模型的目的是为了去除需求中与问题域和应用域无关的东西,抓住问题的本质,便于理解,从而更容易写出高质量的代码。另外,更重要的是,对象模型无论多么抽象,最后要落地变成可运行的二进制程序,成功与否由用户体验说了算。所以,程序员最终的劳动成果是功利的,世俗的,可理解的,可验证的。而艺术作品是主观的,精神层面的,其价值是无法用逻辑证明的。
无论是艺术家还是程序员,都不是为了抽象而抽象,也不是为了故作高深而抽象,抽象是为了简化,把问题弄的更简单。Clive Bell 在《Art》中写道:
“To instance simplification as a peculiarity of the art of any particular age seems queer, since simplification is essential to all art. Without it art cannot exist; for art is the creation of significant form, and simplification is the liberating of what is significant from what is not.”
可见,简化是一切艺术的不二法门。艺术是通过有意味的形式来传递超越语言的表达能力和表达范畴的信息,只可意会,不可言传。因此,只有那些和有意味的形式相关的细节才值得保留。而所有可用语言描述的内容都是无关的,必须删去。
Apple 公司的产品向来以艺术性的设计著称于世。而 Apple 公司奉之若圭臬的设计理念就是简单,力求做到无论是老妇还是稚童皆能很容易的理解和使用 Apple 的产品。在 Apple 公司早期的宣传手册中就明确写道:
“Simplicity is the ultimate sophistication.”
简单的设计必须让产品的外形体现出产品的本质,其他一切皆是多余,为此,必须谨慎的用心处理每一处细节。比如手机的显示器是呈现信息的介质,要在有限的空间尽量最大可能地增加它的面积。强调简单和注重细节现在变成了一切互联网公司产品设计的座右铭。
找到问题的最直接有效的解决方案就是程序员可以创造的简单。在这方面,编程语言的设计是个典型的例子。一般地,编程语言设计的重心是定义数据和过程的最基本的表示,操作与组合,这构成了语言的基础部分,不能也不会轻易改变。而语言的高层次的功能如各种应用程序库都可以从这些基础元素派生而来。数论和欧氏几何都只有五条公理作为基础,其他所有的定理可以从这五条公理中推导出来。类似地,一方面,我们希望编程语言的基础元素越少越好,简单且容易理解;另一方面,又必须要保证这些基础元素足够强大和灵活,能够支撑起语言的其他部分以开发出有用的软件功能。
处理这种简单与强大的冲突的做法还是抽象,把貌似不一样的东西看做一样的东西。比如,scheme 是 lisp 的一种方言,它把过程当作数据,反过来,把数据当作过程。如果过程可以用变量命名,可以用作过程的参数,可以作为过程的结果返回,可以包含在数据结构中,那么过程不就是数据吗?过程抽象为数据,就可以像数据一样被操作。我们引用 MIT 计算机教材《计算机程序的构造和解释》(Harold Abelson, Gerald Jay Sussman, Julie Sussman 著,裘宗燕译) 中的一个平凡的例子来说明过程怎样象数据一样被用作过程的参数:
(define (map proc items) (if (null? items) nil (cons (proc (car items)) (map proc (cdr items)))))
map 根据传递给它的过程参数 proc,对表 items 中的元素做不同的操作。比如,返回表中元素的平方:
(map (lambda (x) (* x x)) (list 1 2 3 4))
结果是:(1 4 9 16)
对每个元素乘一个倍数:
(define (scale-list items factor) map (lambda (x) (* x factor)) Items)) (scale-list (list 1 2 3 4) 10)
结果是:(10 20 30 40)
proc 作为 map 的参数,建起了一道抽象屏障,把对表中元素的处理的过程从提取表的元素 (car) 和组装处理的结果 (con) 的过程分离开来。这样,我们就不需要针对不同的表处理写不同的过程,而只要把对元素的处理过程如平方和倍乘作为参数传递给 map 即可。抽象把变的东西 (表中元素的处理) 和不变的东西 (提取表的元素和组装处理后的结果) 分开来,不变的东西是可重用的,保证了一致性;变的东西是可定制的,提供了灵活性。
另外,schema 把数据看作是满足一组约束条件的过程。如整数、有理数、实数和复数都支持加减乘除的算术操作,于是,把他们统统抽象为支持加减乘除过程的数。上层应用在对这些数做加减乘除的操作时,底层的实现会根据数的类型,调用不同的过程。这听起来,其实就是面向对象编程语言的虚函数、纯虚函数和抽象类这一系列概念的滥觞。
赋予过程一个抽象的名字,过程可以调用其他过程,也可以被其他过程调用。这样,软件就能够实现为由下至上的不同层次,不同的层次使用不同级别的抽象语言。上层的过程调用下层的过程,只要下层提供的接口不变,即使实现改变了,也不会影响上层的功能。同样的道理,软件模块化也是过程抽象的结果。毫无疑问,层次和模块化是管理软件复杂性基础。
过程还可以调用自身,即为递归。自己调用自己,多少让人感到不安。实际上,递归不仅仅是编程语言的功能,更为重要的是,递归是程序员特有的思维的工具和表达的方法,简洁而优雅,许多著名的算法都离不开递归,如回溯算法,快速排序算法,分而治之算法,深度优先搜索算法等。lisp 中大量使用递归,甚至用递归替代循环。
总而言之,lisp 是计算机最早的编程语言,历经半个多世纪,依然枝深叶茂,长盛不衰。所凭借的正是这些化繁为简少即是多的抽象机制,数据即过程,过程即数据;表达式即语句;语句即表达式。
即使具有再好的技术的或艺术的抽象思维能力,程序员和艺术家都还是不可能漫无目的的凭空创造出好的作品。假设一个程序员的目标就是要创造最好的程序,假设一个艺术家的目标就是要创造最好的有意味的形式,最终都会一事无成。这就好像一个探险家要寻找宝藏,像任何人一样,他当然知道,在地球的某个角落一定埋有宝藏,但是没有任何线索和地图指向藏宝之地,结果只能是满世界游荡,一无所获。所以,艺术家以特定的主题作为审美情感的皈依,程序员以问题作为创造引擎的原始驱动力。
我想,每个真正的程序员的脑子里面都有一大堆已解决的和要解决的问题,否则,作为程序员的价值岂不就让人怀疑了。程序员在工作的时候,如果不是在解决问题,就是在制造问题,测试工程师就是帮助发现程序员不小心制造的问题。对于一个困难的问题,通往答案的每一步都可能存在多条路,即多个选项,有的路是显而易见的,有的路是隐晦不明的,整个问题的解空间构成了一幅像迷宫一样复杂的多边有向图。寻找问题的答案就像在迷宫中探险一样,行路难!
聪明的程序员喜欢猎奇和发现,在迷雾重重中寻寻觅觅,从不吝惜自己的脑力和体力。解决问题的同时,享受着思考的快乐。与之相反,拙笨的程序员喜欢生活在熟悉的和舒适的安乐窝里,最好每一步都事先由别人设定,最害怕的就是面对各种不确定,并为之承担责任和风险。这样的程序员只能做一些机械化的工作,不用动脑筋,安全舒适省心。但是,如果有一天,程序员的工作有可能被 AI 替代,他们就会首当其冲地被淘汰。所以,激励程序员的最好的方式,就是不断地给 TA 有价值的问题。
现在,可以回答本文开头提出的问题了。代码对于机器而言,当然谈不到美,因为机器是机械的,没有情感的。但多数时候,代码是写给人看的,这才可能存在美不美的问题。艺术家创造的美是有意味的形式,包括线条和颜色,以及他们的组合。
其实,如果把代码看做一幅画,代码的结构或描述模块和层次的关系就是它的线条或轮廓,代码的模块或层次就是它的颜色。艺术家通过纯粹的形式,从美学的角度去探索形而上的终极真理和终极现实。同样地,程序员借助于抽象,去芜存真,去繁就简,找到问题的最直接有效的答案。
代码的美在于以至简克至繁,水银泻地般从程序员的思维中自然流出,清晰优雅的表达了问题的本质和答案。阅读代码的人象看地图一样,即能从大处概览代码的全貌,按图索骥,也能找到每个层次的每个模块的细节。那么,这样的代码比之那些虽然实现了同样的功能,却混乱的象一堆麦秸的代码,谁能说它不是美的呢? 艺术家的审美情感可以提升程序员的品味,高贵的品味不能容忍粗鄙不堪,观之可厌的代码。我们不一定总能写出最美的代码,但只要不放弃对美的追求,虽不能至,然心向往之,则近道矣。