面向对象与函数式

这其实是一段时间以前在「知乎」的一个回答,获得的赞同还算比较多,放在这里算是给自己的备份吧。

原问题是

对于卡内基梅隆大学计算机系删除基础课程中的面向对象编程课程,如何理解他们提到的 「面向对象编程既是反模块化的又是反并行的」?

下面是我的回答。

这个问题的根本在于 OOP 是基于状态的。每个对象都维护着自己的状态,暴露给外界的是一些可以改变对象状态的方法。一个对象的状态里可以有对其他对象的引用,一个对象的方法也可以调用其他对象的方法来改变其他对象的状态,所以这些状态还是关联的。很多人提到的线程安全与效率的取舍之类其实都是细枝末节,即使是有办法把所有方法都能高效地实现并且全都是线程安全的,只要状态存在,状态带来的问题就存在。在一个复杂的并发系统中,你调用 foo.bar(42),几个指令之后再调用 foo.bar(42),两次调用的结果很可能是不一样的,因为在这中间 foo 的状态可能已经改变了,或者 foo 引用的某个对象的状态可能改变了,不去看 bar() 的实现根本不知道结果依赖于什么。同样一段程序多次运行因为时序的不确定性可能结果也不一样。不管 OOP 也好,过去说的过程式编程也好,理论基础都是图灵机模型,而图灵机就是依靠对状态的记录和改变来进行运算的。图灵机里的纸带和状态寄存器用来记录状态,而读写头用来访问和改变状态。想象一下一个并行的图灵机(多个有独立状态寄存器和不同速度的读写头加上一条共享的纸带)就不难理解在这个模型下并发带来的复杂度。

而目前很多人因为并发的需求所崇尚的函数式编程是基于 Lambda Calculus 的计算模型。计算由层层嵌套的函数调用完成;每个函数调用的结果只依赖于函数和它的参数。如果 f(4, 5) = 10,那么无论你在什么时候调用 f(4, 5),它的结果都是 10。相对而言,这是一个比较干净,比较容易推理和确保正确性的模型。OOP 的程序通常有很多隐藏的数据依赖,函数式编程把这些数据依赖都明确化了。

但函数式编程最大的一个问题是,函数是一个数学抽象,在现实世界中不存在,它必须被模拟出来。目前为止被广泛使用的计算机还是基于图灵机模型,计算机的寄存器、缓存、内存就是用来记录状态的。要真正懂得程序设计,必须知道没有状态的函数是如何在充满状态的计算机上实现的,所以还是绕不开非函数式的编程。另外绝大部分的函数式程序设计语言都不是纯函数式的,出于实用性考虑都夹杂着其他语言的一些特点,并没有完全排斥状态。Haskell 号称纯函数式语言,用 Monad 来抽象状态,理论上可以自圆其说,但在实际使用中其实还是带来了很多不便(于是又发明了 Monad Transformer...)。

从某种程度上说,状态是绕不过去的,毕竟人感知到的宏观世界就是由各种各样有各自状态的对象构成。函数式编程可以帮我们避免很多用其他方式容易犯的错误,在很多情况下写出更高质量的程序,但并发带来的复杂度并不会从根本上消失。各种编程风格一定是互相影响推动程序设计语言的进化,没有绝对的好坏,从 C++ 和 Java 最新标准里引入的函数式方面的功能就很容易看出这一点。比较有意思的是,OOP 最早是在 LISP 里实现的,而 LISP 也被很多人看做函数式编程的起始。同样,好的程序员也会根据具体情况使用合适的编程风格。

OOP 不失为一种比较容易理解的在计算机程序里对现实世界的抽象,在很多场合的应用是非常成功的,至少我没发现以图形用户界面为中心的程序里有比 OOP 更行之有效的抽象方式。把 OOP 从程序员的教育中去掉过于片面和激进了。如果是从基础课程调整为选修课程则是可以理解的,我上本科时记得也是那样设置的。


最新文章

评论

Loading comments ...