從陳述式到表達式

閱讀時間約 5 分鐘

對於程序式編程來說,程式是由一系列的指令組成,例如計算數值、印出訊息、修改變數、呼叫子程序、配置變數的記憶體空間等。定義函式是為了讓一些程序可以重複利用,因此稱為子程序,其中參數為子程序中特別的變數,讓我們能夠透過它們控制子程序的行為。函式的回傳值只是一種方便將結果帶回來的方法,但一般只能回傳一個值,因此早期的c++函式庫常需要預先配置記憶體,再透過參數傳給子程序,讓子程序能夠將結果帶出去。在程式裡面是透過「陳述式」(statement)描述程序應該怎麼執行,其中包含宣告變數、指定變數值、修改變數數值、呼叫子程序等。而為了簡化程式碼,一些計算指令也可以寫成「表達式」(expression),讓多個指令可以縮減成單一陳述式。表達式讓我們不必為每個計算指令的結果設定變數,而是讓表達式本身代表結果。而呼叫函式的指令也可以當作表達式來使用,讓我們可以將更複雜的操作組合起來。但並不是所有的函式都是藉由回傳值帶回結果,很多都是藉由傳入的參考傳回結果,這種函式難以串聯起來,因而仍需要寫成一步一步的陳述式。而且帶有副作用的函式也不適合寫成表達式,因為表達式的執行順序並不明顯,很容易造成奇怪的結果。


對於函數式編程來說,表達式才是描述程式運作的基礎。函數式編程一般只以回傳值帶回結果,如果要回傳多個值就用乘法類型包裝起來,如果要根據狀況回傳不同的值就用加法類型。因此在函數式編程中,大部分的函式都可以透過表達式很容易地串聯起來。函式通常不代表某個程序,它代表的是透過給定的參數計算並組成結果,而且很少會修改參數的值。更抽象地,它還建立了回傳值與參數之間的依賴關係,因此如果我們擔心一個表達式的值受到什麼影響,可以直接找它的參數來判斷。反之,需要寫成陳述式的指令往往會帶有副作用,它會產生隱藏的依賴關係,因此改變陳述式的順序會造成行為的改變。當我們需要共享計算結果時,或是表達式太長需要分段時,也會使用let陳述式定義變數。如果計算過程沒有副作用,這種陳述式可以很容易地重構,例如重新排序、改變變數作用範圍、或是取代此變數成它的定義。因此一個操作有無副作用是很重要的,它會影響我們對於表達式或陳述式的看法,並改變重構程式碼的能力。


對於像是Haskell的純函數式程式語言,變數不可能被修改,也沒有辦法透過呼叫函式執行某種操作,因此根本沒有陳述式的概念,let in並不是陳述式,而且它只能為一個變數綁定值,並不能額外產生其他副作用。通常使用let是為了共用參考,而不是為了執行某種運算。因為沒有副作用,所有的函式都只是通過給定資訊計算出結果,並不能修改變數的值。因為沒有副作用,純函數式程式語言的重構非常輕鬆,這可以讓我們更加專注於內凜的結構,而非實際的語義和目的。非流程控制的陳述式如印出資訊、修改變數等操作則可以用monad實現,Haskell針對monad有特殊的語法糖,使得它寫起來像是陳述式。因為我們必須利用monad才能實現帶有副作用的程序,因此副作用得以明確地與一般的計算分離開來。這種將有無副作用的程序明確分開來看的方法就是純函數式程式語言的特性,同樣的概念也可以帶回一般的程式語言,透過明確說明函式的副作用,可以避免因為未注意的特性產生意外的結果。


因為本來就沒有陳述式,純函數式編程根本沒有一般的跳出流程的方法。它沒有return,函式的身體本身只能是一個代表回傳值的表達式,因此early return的技巧在這裡不管用。如果限定於例外處理的early return,可以用Maybe/Either monad達到相同的效果。return是很特別的陳述式,在這個陳述式之後的程式碼是不會執行的,類似的陳述式是throw等例外處理的機制。另一種說法是它們具有never type,也就是一個永遠不可能回傳的類型。類似throw陳述式的東西在Haskell是throw函式,它會丟出例外並跳出當前的執行程序。這類的函式會回傳Void,代表永遠不可能求值成功。Void可以當作任何類型,畢竟永遠不可能建構這個類型的值,相反的,標榜可以回傳任意類型的函式事實上就是不可能回傳的函式,這是在純函數式程式語言中對於例外的表達方式。


一般的if else判斷式是陳述式,因此這種流程控制方法在Haskell並不存在。Haskell的if then else類似於三元表達式,它會根據狀況切換到其中一個表達式,它們必須都有相同回傳類型。Haskell的條件式流程控制是基於模式匹配,也就是根據加法資料類型的結構控制流程,因此它可以看成是由資料本身決定流程,而非由我們主動決定。例如函式either可以根據Either類型的數值選擇呼叫哪一個函式,這跟case of的作用是一樣的。事實上所有的模式匹配都能寫成這種形式,因此我們根本不需要流程控制的語法。甚至也不需要定義代數資料類型的機制,所有代數特性都可以由特定的函式完整描述,這稱為Scott encoding。這顯示了代數資料類型本身描述的不是資料結構,而是資料的行為,也就是流程的控制。乘法資料結構Tuple可以透過函式uncurry描述,它代表同步的流程;加法資料結構Either可以由either描述,它代表流程的分支;List可由fold描述,它代表迴圈的流程;遞迴的資料結構可由相應的catamorphism描述,它代表遞迴的流程。可以說流程控制在Haskell只是一種特殊的函式,case of/pattern match/if then else只是使用這種特殊函式的語法糖。

3會員
23內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
發表第一個留言支持創作者!