隨著簡單的部分結束
一天一天構思主題變成一件痛苦的事
今天打算來聊聊物件導向與函數導向
雖然這個主題有點超乎我目前的能力所及
但我還是會盡我所能,將我理解的部分呈現給各位
還請大家不吝指教
物件導向是1970年代為了解決當時日漸龐雜的需求,為了讓程式更好維護,同時能夠重複使用而發展出來。一般會把Smalltalk視為經典,但最早在1960年代的Simula就可以發現物件導向的蹤跡。[1]
在物件導向的思維世界當中,程式裡面的每一個元素都可以拆分為獨立又互相呼叫的小單位,這與傳統的程序思維剛好相反:程式是一個函式系列的集合。物件導向一般有下列的特徵:類別、物件、繼承、封裝:
因為畢竟不是教科書,這邊大略介紹一下。
如果物件導向是以物件為基本組成元素的程式設計觀念,函數導向程式設計(functional programming) 便是提倡以函數為基本單元來組織程式[2]。根據「Why Functional Programming Matters」一書表示函数式導向程式語言通常有下面的特性:
不包含變數給值,變數一旦給定,就不會有變化。簡言之沒有副作用。一個函數一旦執行,除了產生結果沒有其他影響。這就消除了一個主要的bug來源,也使得執行順序無關緊要。
與傳統觀念比起來,函數導向非常在意「沒用副作用」這個精神,相同的輸入一定要有相同的結果。並且利用這個特性,將一個複雜的問題,不斷的透過純函式逐層推導出複雜的運算,而不是設計一個相對複雜的執行程序[3]。
函數導向的特性:
介紹到這裡,可能還是有些模糊不清
讓我們來看一個簡單的例子做比較:
// 物件導向
$ "ABCabc".downcase
> "abcabc"
// 函數導向
$ String.downcase("ABCabc")
> "abcabc"
在物件導向的世界中「萬般皆物件」
所以"ABCabc"
是一個類別為String
的物件
因此可以使用String
的類別方法downcase
相對在,在函數導向思維"ABCabc"
只是一個類別為String的參數
我們呼叫String module下個一個function donwcase並傳入參數
最後得到函數運作後的結果
雖然語言本身就已經區分特性
但事實上物件導向與函數導向是一種思維方式
所以就算是物件導向的程式語言
我們依然可以使用函數導向的思維來開發(反之我不確定可不可以)
例如React雖然是使用Javascript的框架
但其中就充滿著函數導向的特性
根據林信良的說法:
物件導向與函數式並不衝突,兩者可相輔相成。當面對職責混亂的物件,可試著以函數式概念對物件的函式進行重構,若一開始不知如何畫分物件職責,可試著先以函式為單元進行設計,再看看函式是否可進一步重構出子函式。當問題被分解為子問題,函式被切得夠細小,回過頭來會發現數個函式間的關聯性,這時無論是使用類別組織資料、將函式搬運至適當類別之中,都會有較清楚的判斷界線,從而實現更高階的物件導向概念。
目前網路框架的後端主要還是以物件導向為主(PHP, Java, Python, Ruby)
函數導向的好像只有Elixir(而且使用者還很少)
說穿了這其實是兩種不同的思維方式
在不同情境下用不同的方式解決問題
而且按照程式進化典範學習的慣例
未來的語言應該會融合兩者的特點而持續進化(例如Scala)
讓我們拭目以待吧!
參考資料:
https://www.ithome.com.tw/node/73705
https://blog.miniasp.com/post/2016/12/10/Functional-Programming-in-JavaScript.aspx
// 物件導向
$ "ABCabc".downcase
> "abcabc"
// 函數導向
$ String.downcase("ABCabc")
> "abcabc"
這函數導向的例子我覺得舉的不好。
String.downcase("ABCabc")裡,String也是物件,downcase()是物件裡的方法。
"ABCabc".downcase則是物件裡的屬性。
函數導向的話,就我的理解會像這樣:
downcase("ABCabc")
純函數導向你會看到像是這樣的東西,拿簡單的知覺器來舉例:
def XOR(x1, x2):
return AND(NAND(x1, x2), OR(x1, x2))
程式是由初始的輸入(x1, x2)來組成,盡量不改變其輸入值,中間不去另外指派(x1, x2),這樣能確保沒有全域變數被亂改的情形。
另外把各種執行階段都拆解成函數也可以很輕易的改成異步執行。
盡量把程式解構成函數也可以確保程式碼的可讀性和可重用性,將主程式拆解成像流程圖一樣,遇到問題再去找相關的函數修改。
也很利於單元測試。
真的純函數導向你還會看到有些神人連if、for都不用,map、reduce、filter就解決一切。
實際上的話你可以把物件導向看成資料封裝,在函數中你可以用物件當成輸入,然後去處理,得到一個傳回值,也可以在物件導向中的方法利用函數導向去解構,其實不衝突。
來幫忙平反兼廣告,在 elixir 的例子裡 String.downcase/1
跟物件無關。String
可以看做是一個 namespace,把一堆跟字串操作相關的純函式蒐集在一起。這東西我們叫做 module
模組。
如果 if
跟 case
可以更清楚表達程式的意圖的話,還是會用啦。只是你會發現如果有 pattern matching,加上你說的那些高階函式,足以解決大部份的問題了,跟神不神人無關 XD
這篇底下的 JavaScript 跟 Elixir 的對比也許就比較符合你的原意?
都平反了,也來一些修正。
延遲評估 (Lazy evaluation):參數傳入時才執行
不, lazy evaluation 是說當真的有需要值的時候,才進行求值。請參考 https://ithelp.ithome.com.tw/articles/10195170
函數式的 Web 框架其實行之有年,例如 Haskell 的 yesod,Clojure 的 Luminus。真的要算 Scala 也有 lift 跟 play。不講框架,史上第一個 web application "Viaweb" (後來的 Yahoo! Store) 就是 Paul Graham 用 lisp 刻的。早期的 reddit 也是 lisp。只是函數式編程從來就是小眾中的小眾。
[個人意見區]
另外我覺得物件導向跟函數式編程是衝突的。這兩種 paradiam 各自的強項,剛好是對方的弱項。只是因為近來每個語言都有 lambda,學習某種程度的函數式編程,可以讓你把一些技巧用在 OO 語言裡。也能學著從完全不同的角度來看待程式如何運作這件事。但是因為本質上的差異,在函數式編程裡用不到,也無法使用 OO 的概念。(除非要用 Erlang 作者 Joe Armstrong 的解釋法,不過那就是另一個議題了。)
不,在可預見的將來,我覺得不太會有融合這種事。Scala 是好一陣子的語言了,個人意見的話,我覺得他反而讓事情變得太過複雜了。
希望有幫上忙~ :D
感謝回饋!
其實我後來在餵小孩的時候,有大概想到為什麼你那時會那樣寫。過陣子整理一下再來這邊回 XD
我們先假設「參數傳入才執行」這句話為真。
在 JavaScript 或是 Ruby 裡,如果我們寫這樣的程式:
y = h(g(f(x)))
其中 x
應該可以理解為脈絡中的參數*。那麼程式會先把 x
傳進 f
求值。得到的結果再傳入 g
,以此類推。這就是 eager evaluation。
* 雖然事實上 f(x)
也是 g
的參數,而 g(f(x))
是 h 的參數。
那麼在 haskell 中,會寫成這樣
y = h.g.f x
因為預設 lazy evaluation,且語法上也明示了,會先合成 h
、g
、f
得到一個函式。再將參數 x
傳入此合成函式,視為開始執行。這樣的話,我們的前提就得證了。
但是我們的前提不管在 eager evaluation 及 lazy evaluation 都是真的,因此重點不在什麼時候執行,而在於什麼時候傳入。