想象一下,你坐在河边,河岸边绿草如茵,不远处是奔腾的河流;想想眼前的这条河是不是真的。果然,几米开外,确实有一条河流奔流而下。但是,我们称之为“河流”的众生到底是什么?毕竟,河流是不断流动的,也是不断变化的。“河”字似乎不能指代任何固定不变的东西。2009年,Clojure创始人RichHickey发表了精彩的演讲,阐述了为什么像上面那样的哲学困境会给面向对象编程的编程范式带来问题。他认为人们在计算机程序中看待物体的逻辑与他们看待河流的逻辑相同:我们想象物体是固定的,尽管物体的许多或所有属性一直在变化。所以,这个逻辑是不正确的,我们无法区分同一个对象实例在不同状态下的区别。程序中没有明确的时间概念。人们只是简单地使用相同的名称,希望对象在被引用时处于预期的状态。这样一来,我们难免会遇到失败的bug。Hickey总结道,解决这个难题的方法是,人们应该将世界建模为一组作用于不可变数据的进程,而不是一组可变对象。换句话说,我们应该把每个对象都看成一条“河流”,因果相连。总之,您应该使用像Clojure这样的函数式语言。作者在漫游中思考了面向对象程序设计的本体论问题。自从Hickey的演讲以来,人们对函数式编程语言的兴趣不断增加,大多数主流的面向对象编程语言都采用了函数式编程语言。即便如此,大多数程序员还是我行我素,继续实例化对象,不断改变它们的状态。这些人已经这样做了很长时间,很难从不同的角度来看待编程。我曾经想写一篇关于Simula的文章,关于我们今天所知道的面向对象的概念何时以及如何应用于编程语言。然而,我认为写出最初的Simula与今天的面向对象编程有何不同会更有趣,我可以保证。毕竟,我们今天所知道的面向对象编程还没有完全形成。Simula有两个主要版本:SimulaI和Simula67。Simula67为世界带来了类、类层次结构和虚拟方法;但SimulaI是一个初稿,它试验了如何捆绑数据和流程的其他想法。SimulaI的模型不是Hickey的功能模型,但它侧重于随时间展开的过程,而不是具有隐藏状态的对象之间的交互。如果Simula67采纳了SimulaI的思想,我们今天所知道的面向对象编程可能会大不相同——这种偶然性提醒我们不要认为当前的编程范式将永远占主导地位。从Simula0到Simula67Simula是由两位挪威人KristenNygaard和Ole-JohanDahl创建的。1950年代后期,Nygard受雇于挪威国防研究机构(NDRE),这是挪威国防军的科学研究中心,隶属于挪威军队。在那里,他负责为核反应堆设计和运行研究设计蒙特卡罗模拟。最初,这些模拟是手动完成的;后来,实验被编程为在FerrantiMercury计算机上运行[1]。Nygard然后发现需要一种更有效的方法将这些模拟输入计算机。Nygard设计的模拟被称为“离散事件模型”,它记录了一系列随时间改变系统状态的事件的进展。但问题的症结在于,模拟可以从一个事件跳到另一个事件,因为事件是离散的,并且事件之间的系统没有变化。根据Nygard和Dahl于1966年在Simula上发表的一篇论文,该模型被迅速应用于“神经网络、通信系统、交通流、生产系统、管理系统、社会系统等”[2]领域分析。因此,Nygard认为,其他人可能也需要更高层次的模型来描述他们的模拟。于是他开始物色人才,帮助他完成他称之为“模拟语言”或“蒙特卡洛编译器”的项目[3]。达尔当时也受雇于挪威国防科学研究中心,专攻语言设计,此时也加入了尼加德的项目,扮演“沃兹尼亚克”(LCTT译注:指苹果公司联合创始人史蒂夫加里沃兹尼亚克)).在接下来的一年左右,Nygard和Dahl共同开发了Simula0语言。[4]该语言的早期版本只是对ALGOL60的次要扩展,当时仅打算用作预处理器。当时的语言比后来的编程语言抽象得多,其基本语言结构是“车站”和“乘客客户”,可以用来对某些离散事件网络进行建模。Nygard和Dahl举了一个模拟飞机离开港口的例子。[5]但Nygard和Dahl最终提出了一种更通用的语言结构,可以代表“车站”和“乘客”,也可以为更广泛的模拟建模。这些是将Simula作为ALGOL专有软件包的地位转变为通用编程语言的两个主要概括。SimulaI没有“站”和“客户”的语言结构,但它可以通过使用“过程”来再现这些结构。(LCTT译注:这里使用的“进程”不同于用来指代当前计算机中一个已执行程序的实体的概念,一般情况下,您可以将本文中提到的“进程”理解为一种“对象””.)一个流程由大量数据属性组成,这些数据属性与个体行为相关联,是流程的操作指令。您可能会将进程视为具有单一方法的对象,例如run()或其他方法。但是这个类比并不全面,因为每个进程的运行过程都可以随时挂起和恢复,因为这个运行过程是协程的一种。SimulaI程序将系统建模为一组在概念上并行运行的进程。事实上,在一个时间点,只有一个进程可以称为“当前进程”。但是,一旦一个进程暂停,下一个进程就会自动取而代之。在模拟运行时,Simula会维护一个“事件通知”时间线,用于跟踪每个进程何时恢复。为了恢复挂起的进程,Simula需要记录多个调用堆栈。这意味着Simula不能再作为ALGOL的预处理器,因为ALGOL只有一个callstack调用栈。于是Nygard和Dahl下定决心,开始编写自己的编译器。在介绍该系统的论文中,Nygard和Dahl借助图表模拟了一家可用机器数量有限的工厂,从而说明了该系统的使用。[6]在这种情况下,这个过程就像一个订单:通过找到一台可用的机器来下订单;如果没有可用的机器,订单将被搁置;一旦机器可用,订单就会被执行。有一个订单流程定义实例化了几个不同的订单实例,但是这些实例不调用任何方法。程序的主体只是创建进程并使其运行。有史以来第一个SimulaI编译器于1965年发布。Nygard和Dahl离开挪威国防科学研究中心后,进入了NorwegianComputerCenter,NorwegianComputerCenter,SimulaI在那里越来越受欢迎。当时,SimulaI可以在UNIVAC的计算机和Burroughs的B5500计算机上执行。[7]Nygard和Dahl与一家名为ASEA的瑞典公司签订了咨询协议,使用Simula模拟加工车间。但后来Nygard和Dahl意识到Simula也可以编写与模拟无关的程序。奥斯陆大学教授SteinKrogdahl曾写过Simula的发展史,说“真正能推动新发展起来的通用语言快速发展的催化剂”是一篇题为《记录处理》RecordHandling的论文,坐车。霍尔,英国计算机科学家。[8]如果你现在读了霍尔的论文,你就不会怀疑这句话了。当人们谈到面向对象语言的发展历史时,经常会提到霍尔的名字。以下摘自Hall的文章《记录处理》:该方案设想在程序执行过程中,计算机内部有任意多条记录,每条记录代表程序员过去、现在或将来需要的某个对象。该程序保持对现有记录数量的动态控制,并可以根据当前任务的需要创建新记录或删除现有记录。计算机中的每条记录都必须属于有限数量的非重叠记录类型之一;程序员可以根据需要声明任意数量的记录类型,使用标识符来命名每种类型。记录类型可以用“牛”、“桌子”、“房子”等常用词命名,而属于这些类型的记录分别表示“牛”、“桌子”和“房子”。霍尔在这篇论文中没有提到子类的概念,但达尔由衷地感谢霍尔指导两人发现了这个概念。[9]Nygard和Dahl注意到SimulaI的进程经常有相同的元素,因此引入父类来实现公共元素很方便。这也加强了“进程”这个概念本身可以作为父类的可能性,即不是每一个类型都必须作为进程,只有一个运行过程。这是Simula语言泛化的第二次飞跃。这时,Simula67才真正成为一种通用的编程语言。正是这一变化,短暂地激发了尼加德和达尔重命名Simula的想法,让人们意识到Simula不仅仅是一个模拟。[10]不过,考虑到“Simula”这个名字已经非常有名,再取一个名字可能会带来很多麻烦。1967年,Nygard和Dahl与控制数据公司ControlData签署了一项协议,开始开发新版本的Simula:Simula67。在同年6月的一次会议上,来自ControlData、奥斯陆大学和挪威计算机中心会见了Nygard和Dahl,以制定新语言的标准和规范。最终,会议发布了《Simula 67 通用基础语言》,为语言定下了方向。Simula67编译器的开发由多家供应商承担。Simula用户协会AssociationofSimulaUsers(ASU)也随之成立,每年召开一次年会。不久之后,Simula67在23个国家/地区得到使用。[11]21世纪的Simula语言人们至今还记得Simula,因为取代它的编程语言都深受它的影响。今天,很难找到仍在使用Simula编写程序的人,但这并不意味着Simula从这个世界上消失了。感谢GNUcim,人们今天仍然可以编写和运行Simula程序。cim编译器遵循Simula标准的1986年修订版,基本上就是Simula67。你可以用它来编写类、子类和虚方法,就像使用Simula67一样。所以,你可以很容易地用Python或Ruby写几行面向对象的程序,你也可以用cim写:!狗.sim;开始上课狗;!cim编译器要求完全指定虚拟过程;Virtual:Procedurebark是Procedurebark;;开始程序树皮;开始OutText("Woof!");出图;!输出换行符;结尾;结尾;吉娃娃犬类;("Yapyapyapyapyap");出图;结尾;结尾;参考(狗)d;d:-新吉娃娃;!:-是引用赋值运算符;编译和运行程序:$cimdogs.sim编译dogs.sim:gcc-g-O2-cdogs.cgcc-g-O2-odogsdogs.o-L/usr/local/lib-lcim$./dogsYapyapyapyapyapyap(你可能注意到cim首先使用Simula语言编译成C语言,然后传递给C语言编译器)这就是1967年的面向对象编程,除了语法上的不同,和2019年的面向对象编程没有本质区别。如果你同意我的看法,你就能理解为什么人们认为Simula在历史上如此重要。不过,我想介绍一下SimulaI的核心概念——流程模型。Simula67保留了流程模型,但只能在使用Process类和Simulation块时调用。为了展示这个过程是如何工作的,我决定模拟以下场景。想象一下,有这样一个满是村民的村子,村子旁边有一条小河,小河里有很多鱼。然而,村里的村民只有一根鱼竿。村民们胃口大开,每小时都挨饿。当他们饿了的时候,他们就会拿着鱼竿去钓鱼。如果一个村民正在等待钓鱼竿,另一个村民也将无法使用它。结果,村民们排起了长队去钓鱼。如果村民要等上五六分钟才能钓到一条鱼,那么这样等下去,村民的身体状况会越来越差。如果再有一个村民瘦到骨瘦如柴的地步,最后可能会饿死。这个例子有点奇怪,虽然我无法解释为什么我会首先想到这样的故事,但就这样吧。我们将村民视为Simula过程,并观察在一个有四个村民的村庄进行一天的模拟会发生什么。完整程序可通过此处的GitHubGist链接获得。我把输出的最后几行放在下面。让我们看看当天最后几个小时发生了什么:1299.45:王五饿了,要了一根鱼竿。1299.45:王舞在钓鱼。1311.39:王舞钓到一条鱼。1328.96:赵六饿了要鱼竿。1328.96:赵六在钓鱼。1331.25:李斯饿了要鱼竿。1340.44:赵六钓到一条鱼。1340.44:李斯饿了,在等鱼竿。1340.44:李斯在等鱼竿的时候饿死了。1369.21:王舞饿了要鱼竿。1369.21:王舞在钓鱼。1379.33:王舞钓到一条鱼。1409.59:赵六饿了要鱼竿。1409.59:赵六在钓鱼。1419.98:赵六钓到一条鱼。1427.53:王舞饿了要鱼竿。1427.53:王舞在钓鱼。1437.52:王舞钓到一条鱼。可怜的约翰尼最终饿死了,但他比早上7点前饿死的约翰尼活得更久。赵六和王舞现在肯定过得不错,因为就剩下他们需要鱼竿了。在这里,我想说明一下,这个程序最重要的部分就是创建进程(四个村民)并让他们运行。各个进程操纵对象(钓鱼竿)的方式与我们今天操纵对象的方式相同。但是程序的主体部分没有调用任何方法,也没有修改进程的任何属性。进程本身有一个内部状态,但是这个内部状态的改变只能由进程自己来完成。在这个程序中,还是有一些域是变化的,这种程序设计不能直接解决纯函数式编程可以解决的问题。但正如Crodahl指出的那样,“该机制指导程序员执行模拟以构建底层系统的模型,生成一系列进程,每个进程代表系统内事件的自然顺序。”[12]我们不是思考主要根据名词或参与者(对其他对象做事的对象)来描述正在进行的过程。我们可以将程序的全部控制权交给Simula的事件通知系统,Crodahl称之为“时间管理器”。因此,虽然我们仍在适当地改变进程,但没有进程可以假定其他进程的状态。每个进程只能间接地与其他进程交互。这种模式如何用于编写编译器、HTTP服务器和其他东西是不确定的。(此外,如果您曾经在Unity游戏引擎上编写过游戏,您会发现两者非常相似。)我也承认,虽然我们有一个“时间管理器”,但这可能并不是Hickey的确切意思,他说。假设我们需要在我们的程序中有一个清晰的时间概念。(我认为Hickey想要的类似于AdaLovelace的上标符号,用于区分变量随时间的不同值。)尽管如此,我们可以发现,面向对象编程的设计方式让我觉得很有趣预先与我们今天习惯的面向对象编程并不完全相同。我们可能会理所当然地认为面向对象编程也是如此,一个程序就是一个长长的事件记录:一些对象以某种顺序作用于其他对象。SimulaI的过程系统表明,进行面向对象编程的方法不止一种。仔细想想,函数式语言可能是更好的设计方式,但是SimulaI的发展告诉我们,现代面向对象编程被取代是很正常的。
