为什么我觉得OOP优于FP?


#1

我们知道面向对象有三要素 封装、继承、多态

首先谈谈封装,我认为OOP有一个很大的创新,那就是它能够显式地封装状态,而封装状态这个概念在过去是没有的(无论是面向过程还是函数式)。

举个例子:有些编程语言有模块(Module)的概念,而模块本质上是一个绑定的集合(他被赋予名字,然后可以被模块外的代码引用)。如果你的语言不支持任何mutable操作则没问题。然而,一旦你引入了mutable,这个模块就不纯了。这时候你可能会想要两个模块,或者模块间通信之类玩意,于是模块就变成了类。。。

当然如果你能保证代码里完全没有mutable,那完全用模块就好了,但是你懂的。。。(mutable再怎么不好,它总是不可避免的存在,现实世界总是那么复杂)

所以类本质上就是模块,只是把绑定的时机从程序初始化延迟到了类的实例化。

接下来谈一下继承,相对于上面的封装来讲,继承的亮点确实少了很多,但仍然是不错的语言特性。利用继承,可以最大限度的复用已有的代码(通过开闭原则)。

值得一提的是:现代OOP语言(比如:Java)同时支持两种继承,普通继承泛型继承。普通继承就不说了,泛型继承其实相当于Haskell类型类和ML函子,所以在代码复用这一点上OOP和FP大致打平,不过现代OOP语言还支持trait。

最后说一下多态,现代OOP语言(比如:Java)同样支持参数化多态子类型多态

以上从三个角度比较,我们会发现OOP其实并不弱于FP。

发现现在很多人无脑黑OOP,所以想为它说几句。

你们怎么看?想听听大家的看法。


#2

见识少才会觉得 OOP 和 FP 是对立的。Ada 用模块实现 OOP,Ocaml 的模块就是函子,这两个语言都支持在运行时产生模块。迄今为止 OOP 和 FP

封装状态在 OOP 之前没有?Closure 了解一下,这个概念在 lambda calculus 还没有具体编程语言实现之前就有了。60 年代初连 Simula 都差点还没有出来,谈何 OOP?

不是泛型能够代码复用,而是为了代码复用才有泛型。或者说 Haskell 是为了能更准确表述 Algebra 中的常见概念,比如 Semigroup,才会引入 type class。和 Haskell98 比较的话很明显可以看出这里的动机。

至于为什么会有人觉得 OOP 不如 FP,只不过是因为 OOP 更复杂,没有出名的研究项目导致没人吹罢了。或者说最出名的 OOP 研究项目实际上就是 Java,集成了几乎所有的先进 OOP 特性(很多少见的特性我只见过搞 Algebra 的才会用到,还是在 Axiom 的 paper 上看到的),结果因为太日常了被人忽视了。C++ 很多方面都不如 Ada,但不知道为什么很多人都以为 Ada 是死语言。Smalltalk 被广泛理解为教学语言,都以为成年人不能用。Simula 倒真是死了,后继有个 research language 叫 BETA,也死了,主要特性被 Java 继承了。

ISWIM 系受 Hindler Miller 的限制对 subtyping 支持不好,在 dependent type 下一样可以支持 parametric 和 subtyping。


#3

说个大家都知道的吧

JavaScript

请问他是 OOP 还是 FP 呢?

FP最简单的特性就是函数作为第一公民 JS是有的

OOP的封装 继承与多态 JS还有


#4

近些年出来的语言很少有单一范式的了,都是混合范式,既能 FP 也能 OOP。
封装状态?闭包搞定。
模块间通信?函数调用呗。
代码复用?我觉得继承复用的复杂程度远高于函数组合。很多平常的复用都可以用函数组合搞定,再复杂一点的复用可以用宏来消灭。而且 Java 中不也提“组合优于继承”么。
多态的话,一般比如用 Erlang 里面的模式匹配,或者 Clojure 里面的层级委派,或者协议,等等,感觉更显式,一看就知道这个方法调用之后被分配到哪里。反而 OOP 里的多态,调用之后很绕。
泛型,我熟悉的都是动态类型的语言,所以理解的不是很深。。。


#5

戛然而止,是不是少了点。。。


#6

沒有统一的定义唄。OOP 有 class based, prototype based, module base 等等先不提,FP 的定义是什么?lambda calculus? Forth 和 APL 那种 tacit programming 同样也算在 FP 里。说两者中哪个好,就和说

四条蹆好,两条蹆坏

一样沒头脑。


#7

话说Erlang不就是OOP嘛。。。


#8

确实没有明确定义。。。 不过FP应该有一个必要条件,那就是无mutable,因为一旦引入了mutable,你可能就会想要 两个这样mutable对象mutable对象通信时序 之类的概念。


#9

编程语言不管怎么变, 能力也好还是哲学概念也好, 编程语言的本质是, 建立代码和现实问题模型的桥梁, 如果编程语言本身的哲学理念和库能够极大简化解决现实问题的复杂度, 这门编程语言就是好的, 适合某一领域.

比如写硬件驱动, C是保持性能又足够抽象的语言, 面对驱动编写简单直接又可靠, 函数式编程你再怎么吹牛, 你绕一个大圈圈还不是写底层状态, 那又何必绕圈圈呢?

Web编程大多数都喜欢动态脚本语言, 现在浏览器这个runtime优化足够好以后, JavaScript怎么动态性能也不差, 你看 NodeJS 比 Python/Ruby 的性能还好, 所以JS做Web编程就是爽, 语法不严谨是不严谨, 但是简单, 门槛低啊, 看着葫芦画瓢画多了, 自己也成为高手了.

大家都嫌 Lisp 括号多, 但是你看 Elisp 写编辑器插件多么得心应手? 状态/表达式和编辑器处理语法树严格对应, 你甚至都可以按照语法树解析的步骤, 一个一个 monkey patch 的边调试边写, 等所有状态写完了, 插件也就做好了.

那些 OOP 语言再怎么吹工业级别的软件质量, 但是一旦还要写编译器的时候, 函数式的威力就出来了, 极大的简化了编译器的各种状态和解析器的构建, 你看当年唐凤单刀用 Haskell 写了 Perl6 编译器, 就知道函数式语言的威力在什么地方了.

等你学了几十门编程语言以后, 拿七八样编程语言在各种现实场景干活解决问题以后, 你就会知道, 编程语言不过就是工具, 没有最好的编程语言和最好的编程理念能够解决所有场景的问题, 也不要天真的认为一门编程语言通吃天下, 编程语言本身是工具, 哪门编程语言解决现实问题越快, 代码越容易维护, 就去使哪门编程语言, 重要的不是编程语言, 而是锻炼自己理解现实需求和抽象代码的能力, 这才是最优的方式.

为什么大家会问这种问题? 因为很多时候, 大家觉得好的东西, 并不一定是真的好, 而是这些人太会写, 加上你自己读书少没文化的时候, 就会被他们洗脑, Haskell的函数式编程思维(无锁, 副作用控制, monad等), DHH的约定即框架, Linus的C++就是垃圾语言, Golang和Rust各领风骚的宣传, React/Angular/Vue.js 一个Virtual DOM就能忽悠很多人.

不是说这些技术和宣传不好, 关键是要保持头脑清醒, 多读书, 多锻炼, 自己的能力和独立思想才是关键.


#10

虽说Closure也能封装状态,但是默认是希望你不要去改他(改Closure是一件很可怕的事情)。。。

因为一旦你改了Closure,就会出现诸如"两个Closure"的问题,然后你就要思考到底把"两个Closure"放哪里的问题,是放在参数上 还是 放在模块里(作为全局变量)。

如果你放到模块里,那么就会出现我前面提到的"两个这样的模块" 以及 “模块间通信” 之类的问题,最终不得不发明OOP。

如果放在参数里,则会导致函数的参数列表变得很大(而且一旦参数都mutable了,函数的返回值的意义就会变得很尴尬,这还是FP吗?)


#11

一个函数的参数列表大的时候,就拆成两个函数。想在函数(模块)之间传递消息,就只能通过参数传递。
或者真的需要共享变量的时候,引入公共的变量,比如数据库,redis,Erlang 里面的 gen_server ETS,各种语言的 actor 模型都可以做这个事情。


还是楼下专业,linear logic,我都没听说过 :joy:
和楼下互动一下, gen_server 其实就是 continuation 保存状态。


#12

用 linear logic (简单讲就是在语言层次限制同時引用)可以在「线程安全」前提下操作 mutable。Clean 和 Rust 都用了这个思想。它們都算 FP。

有了 Closure 就能有 continuation,「状态」可以以函数调用的参数的形式保存在 continuation 中,而不用引入 mutable。

當然我承认这樣用很不直覚,不過我想表達的是它們只是同个東西的不同名字而己。OOP 和 FP 是看待事物的两种方法,本质上的東西都是相通的。

这年头用 continuation 搞分布式是基本操作。


#13

你这里说的continuation具体指什么? 我不认为continuation和mutable有什么关系。

如果你说的continuation指的是程序里的延续的话,closure 能实现continuation,难道对象就不能实现? closure 相比对象还有个缺点,那就是closure是匿名的(一个lambda匿名对象),不利于显式序列化。

举个例子:假如你的程序执行到一半需要关机,明天继续执行。 这时候就需要显式序列化continuation,如果你用closure实现continuation的话,就很麻烦了。因为closure的defunctionalization是由编译器完成的,你不能显式控制,但是面向对象可以。


#14

我erlang用的不多,但是gen_server不就是面向对象思想的体现吗?


#15

如果不介意以特定语言为例的话,Common Lisp (with MOP)中 closure 同样是对象,同样可以给 closure 名字,同樣可以做 serialization。

对于编译器在运行时可用的语言來说从來不是问題。


#16

把状态放进参数中做递归

server(status) {
  new_staus = 从哪里搞来一个要保存的东西(比如 Erlang 里的消息)
  server(new_staus)
}

这样一直递归,就一直保存着某个状态。而且没有引入变量。


这只是消息传递和函数调用而已。封装继承多态里,也就占一个封装。只有封装不能和面向对象划等号(个人理解)。


不是太理解啥叫显式序列化。论序列化,不可变更容易序列化吧。


#18

你这个提问让我想起来以前西方学者对自然语言类型好坏的争论…

以前的西方学者总觉得以日语为代表的黏着语是最先进的语言, 因为它的语法结构分明, 优美. 而汉语为代表的分析语的语法结构混乱不堪, 然后随着研究的逐渐深入, 又觉得分析语足够简洁, 表现力也不差. 又开始推崇英语和汉语为代表的分析语…

不同类型的语法本身投 有高低优劣之分,每种语言都源于遥远的古代,经过漫长的发展过 程,它们的语法规则都有效地为人们的交际服务。
- 《语言学纲要》 P123

FP和OOP都有自己擅长的范围,现在的工业语言都是具有多范式的,就是为了让开发者怎么方便怎么来。要不,你用纯OOP来演示一个FP里的高阶函数的作用看看?估计有匿名对象可以用都不如fp直接写lambda简洁,同理,OOP的继承和多态更加符合人们认知的直觉,FP的函数组合反而让人觉得抽象。


#19

你的这个模式本质上就是Java里的class(Erlang 里的消息对应Java里的方法),只不过Java使用点记号 foo.bar(),而Erlang使用message passing send foo 'bar

Erlang的这种进程模型有一个缺点,就是如果你的消息和状态转换处理不好的话会造成死锁,而同样的情况下Java会造成死循环(显然死循环优于死锁,因为死锁更隐蔽)

但是我太不理解,这个 status和continuation有什么关系?


#20

你写的这个 mutation只是局部的,局部mutable变量并没有问题。

我说的mutation主要是指有一定生命周期的mutation,如果你的代码里有那种mutation的话,那就不是FP了。


#21

有些语言规定的太死,又太繁琐,对于编程就是障碍,比如java.有些语言太灵活又太过简洁,需要背下来很多字符的意义,这也是障碍,比如bash,perl

重要的不是面向对象还是函数式,而是编程语言是否能在保持足够灵活性的同时又不给程序员的大脑增加太多额外的记忆和分析负担.