《SICP》读后感:关于软件本质的一点思考

摘要:软件本身不是目的,人类的需求才是目的,而软件只是达到目的的手段。
软件的本质在于控制复杂性,这个复杂性并非来自于计算机,也并非来自于现实世界,而是来自于人类的思维和知识体系。
软件被使用的广泛性,在于它所满足的人类需求的广泛性。

什么是软件?

从一个简单的例子说起,比如我想计算两个数的和,于是写下这样的python代码

    print a + b

但是,这段代码是我的最终目的吗?显然不是,我需要把它在计算机上实际运行,并赋予a和b实际的数值。也许我是在水果,买了5块钱的苹果和10块钱的香蕉,然后计算一共需要支付多少钱。

可以看出,软件是我们为了达到某种目的,而指挥计算机如何去完成这个任务的一系列指令。我们的目的是满足某种需求,而软件只是一个手段,显然完全可以通过其他的手段完成这一任务。

SICP中指出,计算机科学和计算机其实并没有本质联系,而是人类知识的一种组织形式,重在对过程性知识的形式化。如同数学是对说明性知识的形式化。也就是说,计算机科学是关于“如何完成某某任务”的知识的记录工具。

看到这里你也许会觉得我说了半天废话——因为人类所有的生产活动,都是为了满足人类的某种需求。不过这一观点将是本文的基本出发点。接下来我们简单分析一下软件作为一种手段,为什么存在,以及有什么特点。

为什么要有软件?

如果软件是为了满足人类的某些需求,那么软件存在的原因就很直接——因为计算机不能直接满足人类的需求。其中的距离来自于计算机的通用性、单一性和人类需求的特定性、多样性。从原理上讲,计算机只需要0和1两个符号,以及NAND(与非)或者NOR(或非)一种运算就够了。而人类的需求则千差万别。

但是,前面说到,满足需求有多重方式,比如人工计算,或者直接使用硬件搭建电路完成某些功能(而这实际上也是电子领域早期的方式)。那么,人类为什么选择软件这种手段呢?其实很简单,原因和其它的手段一样,不外乎成本。从经济学角度,成本,也就是人类的工作时间。(金钱,可以看做你从别人那里买时间)。经济的一个基本规律是规模效应,生产规模越大,单位成本越低。硬件做的越通用,就越适合大规模生产,就越能降低成本。当然,这一切都要建立在下面的基础之上:

生产软件要比生产硬件的效率高。

软件易于改动,另外有一个重要特点——它一旦被生产出来,就可以几乎零成本的进行重复利用。这是一个能极大提高全人类生产力的方式。试想一下,假设你能写一个软件炒一盘鱼香肉丝,那么全世界的人无论谁想再生产一盘鱼香肉丝,只需要简单的调用一下这个软件。很多软件最初并不是写出来给大家用的,而是为了解决自己的实际问题,比如为了简化工作流程,开发某个产品,验证某个科研思路等等,软件只是个副产品——这么说也许不太恰当,应当说软件是解决问题过程的完整记录。而完成任务之后,软件就可以被别人复用了——全人类只需要一个人造轮子,完全消除了重复劳动,多么高效!

所以下面要谈谈代码复用的问题。

软件复用

好的软件不会消失,而会被移植到新的平台上。

                               —— 《Linux/Unix设计思想》

复用软件的成本比生产软件要低的多。所以,软件复用是提高生产率的重要手段。如何更好的复用软件,以及如何编写容易复用的软件?要研究这个问题,需要先考虑另一个问题:软件为什么可以复用?

仍然从基本观点出发:软件是满足人类需求的手段。所以,软件的复用,实际上是人类需求的重复性。考虑两个需求,a)计算一组实数的平均数;b)计算高一3班全体同学的2015年数学期末考试平均成绩。显然,需求a是一个更广泛的需求,并且可以预见这一需求在未来会一再的出现。而需求b则是一个很特定的需求,它只会在2015年期末出现一次。如果有两个软件分别满足两个需求,那么满足需求a的软件将会一再的被重用,而满足需求b的软件将会烂在硬盘上。

所以,为什么“好的软件”会被移植到新平台上?因为它们满足的是人类重复需要的需求,无论技术怎么发展,只要这些需求存在,相应的软件就会存在,只是以不同的形式出现。

所以,为了编写能够复用的软件,我们需要关注的是需求。然而,不幸的是,实际中的需求往往是特定的、易变的。比如,公司要在今年双十一推出一项特定的活动,那么单纯为这个活动所编写的软件,在活动结束后就失去了作用。

要解决这个问题,需要对软件进行层次划分。不同层次的软件,通用性和特定性不同。底层的软件单一、通用,重用度高;而上层的软件则特定性高,生命周期短。为了研究这个问题,需要考察另外的问题——复杂性与抽象。

复杂性

软件的首要技术使命是管理复杂度。

                                          ——《代码大全》

为什么会出现复杂性这种问题?仍然从基本观点出发——复杂性来自于“计算机仅能提供0、1运算”和人类需求之间的距离。人类的一个需求,可能需要成千上万的0、1运算才能完成,而人脑——很遗憾——只能同时处理7个左右的事物。可以说,软件的复杂性,实际上来自于人类需求的复杂性以及人脑处理能力的局限。

实际上,这种复杂性不仅出现与软件领域,而是人类所有知识和概念体系的共同特点。在SICP中,引用了洛克的一段话,对此有提纲挈领的论述:

心智的活动,除了尽力产生各种简单的认识之外,主要表现在如下三个方面:
1)将若干简单认识组合为一个复合认识,由此产生出各种复杂的认识。
2)将两个认识放在一起对照,不管它们如何简单或者复杂,在这样做时并不将它们合而为一。由此得到有关
它们的相互关系的认识。
3)将有关认识与那些在实际中和它们同在的所有其它认识隔离开,这就是抽象,所有具有普遍性的认识都是这样得到的。
                                                   —— John Locke 1690

软件开发的过程,就是将简单元素组合为一个复杂元素,再将复杂元素抽象为简单元素,这样一个不断“组合->抽象->组合->抽象 ……”的迭代过程。

抽象

基于上面的讨论可以看出,软件开发过程中的抽象,本质上是人脑中概念的抽象。这种概念的结构映射到代码上,就成为软件。那么,怎么样是一个好的抽象?好的抽象应该在错综复杂的事物中分解出不变的部分,将它和易变动的部分隔离开。而软件的变动,则取决于人类需求的变动。也就是说,一个好的抽象,也就是一个好的概念,应当反映出人类需求中稳定不变的部分。这样的软件,就会在空间和时间上达到更广泛的复用。

无论过程式、面向对象还是函数式编程,都是提供了一种看待现实世界、进行抽象的方法。如果我们明白了软件结构设计的本质在于为问题设计一个良好的抽象概念体系,那么这些方法之间其实并无本质区别,只是各自具有优缺点的具体的抽象办法而已。

下面我们来看一下计算机科学领域所采用的一些最基础的抽象。

硬件层面

为什么硬件也算抽象呢?因为我们讨论的起点是布尔代数,这是现代电子计算机的数学基础。硬件层面对0、1运算进行了第一层抽象,即一系列的CPU指令,将原本只能进行简单“与、或、非”运算的概念,抽象为提供一系列字节、整数、浮点数、地址等概念。

计算机语言

计算机语言是在硬件层之上的第一层抽象,而本身又分为不同的层次。计算机语言是一种概念抽象方式,所以它是为人设计的,而不是为计算机设计的——因为,如果要指挥计算机做事情,我们只需要“0”、“1”两个符号就够了。计算机语言将硬件提供的功能抽象为若干数据类型的运算、流控制结构、函数等概念,以组成更复杂的软件。可以说,编译器是最大的一个软件复用的例子。然而,这儿不妨思考一个问题:在这个抽象的过程中,是否丢失了什么?不同语言的抽象方式是不一样的,由此所能提供的功能也是有所差别。

操作系统

操作系统是又一种抽象方式,它把一些常用的操作封装为系统调用,这些操作是软件开发中大量的重复性的需求。

各种软件包、框架等等

软件包、框架等不是计算机语言的一部分,而是使用计算机语言编写的软件模块。然而从抽象的角度来说,使用语言构建的模块,和语言本身提供的特性,并无本质区别。同一种数据结构,在某种语言里属于语言本身的一部分,而在另一种语言里则需要使用软件包。然而无论那种情况,并没有本质区别。

业务代码

业务代码是最终完成现实任务的代码,它们最具体、最不具有一般性,所以过时最快。大部分公司的硬盘里沉睡着这类代码。

再论软件复用

越通用的软件,可复用性越强,但是不能直接用来解决实际问题;而满足特定需求的软件,又不具有通用性,如果没有将其中一般性的模块抽象出来,那么将很难复用。既然每个人有自己特定的需求,而软件开发者不可能为每个人开发一套软件,这就要求任何软件都要提供“组合-抽象”的能力,供软件使用者对满足自己的特定需求的功能进行抽象——这种功能可以叫个性化,或者二次开发,或者插件等等。下面举几个具体例子:

  • linux系统提供了一系列ls, grep等工具,它们是通用的。我现在有个需求,检查某服务的log中是否有error,如果有则向某个特定邮箱发送邮件。当然我可以依次手动运行这些通用工具,但是更好的方法显然是写一个shell脚本,并命名为check_log_err.sh。这样每次只要运行这一个命令就够了。这就是一种抽象。

  • 编辑word文档时,有个特殊需求:将每段的第一个字的字体增大一号、加粗并设置为红色背景。显然我也可以每段每段的进行操作。更简单的方法是录制一个宏,然后在每段上回放这个宏。宏提供了一种抽象手段,将“第一个字字体增大一号、加粗并设置为红色背景”这几个操作抽象为一个操作。从这里大家大概已经能看出,从抽象的角度上讲,宏和脚本没有本质区别,都是实现抽象的手段。实际上,word的宏也是以VBA代码形式保存的。只是对于很多非计算机专业用户,写代码是不方便的,而宏用起来比较直观。

  • 用浏览器下载文件的时候,它每次都会询问存放位置。我想把它放在某个download目录下面,于是设置了一下下载路径,这样就不需要每次设置存放位置了。这也是一种抽象:通过配置文件,将通用的“下载”、“存盘”两个功能组合起来,并提供具体参数,来满足我的特定需求。

  • 还有各种插件,比如chrome、sublime text、foobar等,提供了二次开发的能力,这样用户可以针对自己的特定需求进行开发。从这个意义上说,软件的二次开发功能做的越好,生命力就越强。

从抽象的角度讲,任何软件都应当具有二次开发的能力:使用者将软件提供的若干个相对通用的功能,组合、抽象为一个功能,来满足自己的特定需求。这种组合、抽象的功能越强大、越方便,软件的生命力就越强。

小结

本文的主要观点总结如下:软件不是目的,而是手段。人的需求才是目的。在布尔代数和人的需求之间,存在着巨大的距离;而人脑处理能力是有限的,这就产生了复杂性。控制复杂性是一个“组合->抽象”的不断迭代过程。良好的抽象应当满足人类普遍的、长期的需求,这样的抽象以及相应的软件将实现更大程度上的复用。

扩展:关于人工智能

在这个基础上,笔者思考一个问题:未来是否会出现人类理解不了的人工智能?沿着本文的思路,不妨这样考虑:人工智能建立在计算机的基础上,那么它就不能超越布尔代数;而人工智能也是一种软件,它的目的是满足人类的某种需求。但是,中间的过程可能是不一样的,也就是说,人工智能可能会采取和人类不同的抽象方式,或者它根本就不需要抽象。大量的中间层次,可能是人脑不能理解的。但是至少从基础的层面上,人工智能只要还构建在图灵机之上,那么它的基础——0、1运算至少还是能被人脑理解的。

原文地址:https://www.cnblogs.com/aquastone/p/sicp-thinking.html