1. 程式人生 > >面向對象、函數式編程與並行

面向對象、函數式編程與並行

情況 mon 記得 鏈接 情況下 界面 transform foo 容易

作者:江宏
鏈接:https://www.zhihu.com/question/19728806/answer/18575066
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

這個問題的根本在於 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 從程序員的教育中去掉過於片面和激進了。如果是從基礎課程調整為選修課程則是可以理解的,我上本科時記得也是那樣設置的。

面向對象、函數式編程與並行