为什么我觉得OOP优于FP?


#22

死锁指的是两个进入临界区都不释放资源。Erlang 里面就没共享变量,是无锁的并发模型,不存在死锁。只是一个不会停下的递归而已。
仅仅只是调用的话算不上面向对象,面向对象和支持类似对象的概念是完全不同的。OOP 指的是 面向对象 编程。
不是一个语言里面有类似对象的东西,就能叫面向对象编程。就好比一个语言里有函数,仅此不能就叫他函数式编程。
我理解的面向对象就是把各种东西都看做对象,用对象之间的方法进行交互,不去直接操作属性。通过继承来实现代码复用。通过继承来实现多态。
然而函数式里,是通过函数调用来进行交互,调用的时候把需要保存的“状态”手动放进参数里,而不是隐式的放在对象中。通过函数组合实现代码复用。通过模式匹配实现多态(估计有什么更专业的名词?)。而这些都是更加显式的东西,不需要去记忆什么实例变量,继承,重写,重载之类的规则。

这里的 continuation 指的就是在递归的时候,需要记忆上一次递归结束时候的状态,以便进行下一次递归。由于不引入变量,所以没办法把上一次的状态存放到外层作用域的某个值中,所以只能随着递归参数交给下一次递归。这样一直下去就可以实现你说的:

同时没有引入变量,也没有引入对象,也没有引入继承。所以算不上 OOP。如果你硬要说是,那就是定义问题了。那 OOP 和 FP 本来就是大统一的。
而且 Java 里发生了死循环是会大量消耗 CPU 资源,但是 Erlang 不会。这个就是语言细节问题了。


#23

是的,不是“死锁”而是 “一个不会停下的递归”,可能是我解释的不够精确吧。

我的意思是: 如果你的消息和状态转换处理不好的话,Java会立刻死机,这样有利于开发者定位错误并改进设计。而Erlang则可能会把这个问题隐藏,它甚至还能正常运行,开发者未必会意识到自己的错误,直到最终发现数据不一致。

其实Erlang也有引入变量和对象,只不过在Erlang的语言里叫做地址,而这个地址通常会被保存到Mnesia数据库里。

这相当于Java里定义一个叫做ServiceLocator的Singleton对象,然后用它进行对象查找,而不是依赖注入。


#24

觉得刚才描述的不好,我重新写一下。

首先,会立刻死机然后意识到自己错误这和 OOP 的优点没有关系。而且我觉得不死机是好事情啊,动不动就死机是个啥意思。
其次,你说的都是可以用种种方式模拟对象的操作 — 把状态和操作状态的方法封装起来,称作一个对象。这正是从一个侧面说明,FP 和 OOP 是可以结合的,是不矛盾的。
最后,完全可以不引入变量来实现 OOP 的那一套操作。变量有时候的确很方便,但有时候又充满了危险的陷阱。


相比较使用 变量对象继承,我更倾向于使用 不可变量函数组合(高阶函数)。原因是我认为后者更可控,更显式,更清晰。


#25

可以给个简单的例子吗,感觉十分有趣。但是我的只懂cl的一点皮毛 :smile:


#26

#27

封装状态就一定要mutable吗?Haskell那套state monad用起来不也是很爽。。。

函数都写成s -> (a, s)的形式,接受一个旧状态计算之后返回一个值和新状态,在函数之间传递状态就是把这个函数返回的新状态传给当参数传给下一个函数,至于参数多的问题实际上就是函数组合的问题reader monad和state monad都给你解决掉了。

每个函数就是

data State = State {someState :: Int}

foo :: (MonadState State m) => Int -> m Int
foo a = do
        oldState <- get
        let newState = doSomething oldState
        put newState
        return a + 1

这样的感觉,一个“模块”就等于是一个大的runStateT,

moduleMain initState = flip runStateT initState $ do
                                                  y <- foo 1
                                                  bar
                                                  ...

这样bar也可用get获得foo产生的新状态,这就解决了“一定生命周期的状态“这个问题,整个do block里的函数都可以通过get获取当前的状态,用put来更新状态。然后实际上每个函数都是根据传进来的状态计算一个新状态,所以也没有什么mutation、线程安全的问题了。模块(线程)间通讯用Control.Concurrent.STM.TChan啥的,反正monad transformer可以随便套。

Erlang/Elixir的GenServer其实思想也差不多

def handle_call(:pop, _from, state) do
    do something....
    {:reply, result, new_state}
  end

也是接受当前的状态然后返回新的状态

FP的问题我觉得就是组合的问题,两个函数都需要访问一些特定的状态,怎么把它们组合起来。如果是手动组合的话就要把从函数的返回值提取出返回的状态然后传给下一个,就会导致所说的“参数列表很大”这样的问题,但是其实FP那边也有无痛解决这些问题的方法。一定要套OOP的思想多少有点先入为主了。(状态这种东西实际写程序(不是教程里面那种toy example)还是挺常用的,如果FP搞了这么多年还没拿出点无痛写状态的方法就说不过去了)


#28

对FP了解不多,具体是什么方法?


#29

比如各种 State Monad。


#30

现在的语言,一般支持多个编程范式(面向过程,面向对象,函数式编程),只是不同的语言各有侧重而已。

OOP本身是很好的范式,但我觉得自C++以来到现在大行其道的java,对OOP的实现,有点过于偏激。比如,代码复用这一块,实现的方式有很多种,偏偏C++和java这类语言主要利用显示继承的方式来完成。这就造成代码随着工程的进展膨胀起来。很多时候,不得不借助于设计模式的组合模式来规避继承导致的问题。golang之类的语言通过 mixin的方式,我认为能更好的体现OOP的本质。

很多时候,大家抨击OOP,大部分是人云亦云而已,传递错了抨击的目标。我估计一开始并不是抨击OOP,而是抨击java这种语言,偏离了OOP的设计本质。看看java项目里,多少框架都在强调设计模式的重要性, 设计模式是个好东西,但C++和java基本上是用它来解决语言本身的缺陷而使用的,这就本末倒置了。

另外,我认为Elisp虽然是多范式编程,在FP这一块,它应该支持的比较好。但用了ruby之后,感觉ruby在函数式编程这一块更上心。比如,reduce和filter这种非常通用的工具函数elisp里都没提供,还得自己去实现。

至于主题上说OOP比FP好,我认为有些片面,看是要解决什么问题了,若是写个解释器的话,OOP是不是就差点意思了?因为解释器需要用到树形结构,虽然任何编程范式都可以实现,但FP是先天性递归处理,写法上非常便捷。

以上是我个人见解,不知说的对不对,欢迎大家来反驳。


#31

Reduce 和我filter在cl-lib和seq.el里都有提供


#32

我看过一篇文章,里面建议不要用cl-xxx,但不理解为什么,难道有什么冲突吗?


#33

过期的旧文章。旧版本的 Emacs 实现得不干净,会冲突已有的函数。现在基本不会有这种顾虑。


#34

原来如此,明白了,多谢!


#35

不用cl-lib写elisp基本是不可能的。别说subr-x这种外围库有编译时cl-lib依赖。bytecomp.el和byte-opt.el 也依赖cl-lib


#36

如果要描述AST的话,FP用代数数据类型,OOP可以用子类型。

另外,我这里讲的OOP主要是指通常意义上的OOP语言(比如:Java、C#等),他的敌人主要是不开扩展的Haskell、ML等。(Elisp、ruby等动态语言不在比较的范围之内,因为没有比较的意义)


#37

不开扩展还叫 Haskell?