Haskell簡介[0]--什麼是函式程式設計?

Haskell是一個強靜態型別純函式程式語言,於1990年自Miranda分化出來。
在開始介紹Haskell以前,我們先來聊聊Haskell最重要的概念:函式程式設計(FP, functional programming)。

回到程式語言的歷史

從程式語言被發明開始,程式的邏輯便在不斷變遷,先後出現了以下幾種典範(paradigm):

  • 指令式程式設計:最直觀的想法,直接對CPU下達各種指令,例如Assembly。
  • 結構化程式設計:為了避免goto,加入了迴圈、選擇等概念。例如Pascal。
  • 程序式程式設計:將結構化的概念加以延伸,將大問題分化為許多小問題,利用子程式(subroutine)或函數進行處理,例如C語言。
  • 物件導向程式設計:在程式碼越來越長的情況下,為了減少開發和維護的困難,我們試圖將城市的各個功能去耦合。於是人們將小問題封裝成一個一個的物件,若能好好做到SOLID,那你只需要在意物件的介面(Interface),不需要管他的實作。例如Java。

但在現今,物件導向卻又遇到了瓶頸。在執行同步的程式時,由於每一個方法都有可能會存取或修改屬性,故有可能導致競爭條件(race condition)的產生。而解決這個問題最簡單的方法就是利用純函數的特性。

純函數

  • 相同的輸入會導致相同的輸出,與其他隱藏訊息、I/O外部輸入無關。
  • 不能有任何函數副作用,如更改輸入值以外事件、I/O輸出等。

由於兩個函數之前不會有任何交互影響,那自然也不會有競爭條件的發生。也因此,目前許多主流技術都吸收了不少FP的概念。例如Redux, RxJS, Swift等各種語言/函式庫都一定程度地受到了FP的影響。

但以上討論僅止於他在商業上的應用。關於FP背後概念的發展歷史,請參閱下一篇文。

函式程式設計的特性

以下可能會用一些Haskell的程式碼,不過應該猜得到它是什麼意思(吧。

第一等函數與高階函數

所謂第一類物件,指的是支援以下操作的物件(摘自維基百科):

  • 可以被存入變數或其他結構
  • 可以被作為參數傳遞給其他函數
  • 可以被作為函數的返回值
  • 可以在執行期創造,而無需完全在設計期全部寫出
  • 即使沒有被繫結至某一名稱,也可以存在

而被視為第一類物件的函數稱為第一類函數或頭等函數。
高階函數指則的是以函數作為引數或回傳值的函數。
其實除了純函式程式語言以外,其實大多數程式語言都有第一等函數與高階函數。

(幾乎)所有函數都是純函數

純函數的定義如前一節所述。由於純函數的特性,編譯器可以做以下的優化而不用擔心程式出錯:

  • 如果一個函數的返回值不被用到,則他不需要被計算。

  • 記憶化 (memoization):儲存大計算量函數的返回值,並在需要的時候從緩存中提取數值。

  • 消除公共子表達式 (CSE):
    若函式中有重複的子表達式,則可以只計算一次。

  • 平行處理 (parallelism):如前一節所述,排程時不需利用mutex等黑科技,畢竟兩個不同的函數運行是獨立的。

  • deforestation:可以將求值過程中的樹狀結構(包含串列)去除或省略。例如以下程式碼[1]

    1
    all p xs = and (map p xs)

    的計算途中會產生一個串列map p xs,但實際上在計算時可以每計算一個p x就做一次and,便不需要中間的串列。

  • 懶惰求值如後述

至於為什麼是「幾乎」呢?從純函數的定義你會發現,他不允許I/O的存在。至於Haskell要如何處理I/O,又不損失純函數特性的方法,應會在之後介紹單子(Monad)時提到。

沒有變數與賦值

從純函數的定義,你可以發現既然不會改變任何變數,那變數就沒有存在的必要了。在純函數程式語言中,只會有定義,沒有賦值,因此定義皆為常數。

柯里化 (currify)

事實上在包含Haskell在內的大多數函式程式語言,皆僅有單變數函數。那要如何達到多變數函數的效果呢?

以整數加法為例,首先我們先來看看一般定義中,+的型別。

1
(+) :: Int, Int -> Int

但在Haskell裡,我們只能一次給一個參數。當傳入一個參數時,他的型別會變成什麼呢?

1
(+3) :: Int -> Int

當傳入一個參數3時,他變成了一個單變數函數,將任何數字n打到n+3。由此可知,+其實也可以視為一個接收Int,回傳一個Int打到Int的函數。因此在Haskell中,+實際上的型別是[2]

1
(+) :: Int -> (Int -> Int)


你可以發現,任何一個接收型別ab並回傳c的函數的型別會是a -> b -> c[3]。這個將多變數函數以單變數函數表示的方法就稱為柯里化。

懶惰求值(lazy evaluating)

大多數非函式語言的求值策略都是嚴格求值,亦即在呼叫函數前,會先將其引數的值計算完畢。但大多數函式程式語言採用的是懶惰求值,此時只會在需要此值的時候求值。例如以下的程式碼中,

1
length [1 `div` 0]

會回傳1。這是因為在計算length時並不會需要用到串列內的值,因此不會被求值。但當你執行

1
[1 `div` 0]

卻會得到*** Exception: divide by zero。因為這時需要串列內的值了。此時串列內的值才會被計算。這種求值方法允許了無限長的串列的存在,例如以下程式碼:

1
take 5 (map (*3) [1,2..])

會終止,並回傳[3,6,9,12,15]

結語

明明下禮拜就要出國了還在做這種事= =
真的有夠可悲。

這就是第一(零?)篇Haskell簡介了。下一次的內容基本上會著重在形式系統、λ運算跟組合子邏輯,可能會比較數學(?。
也可能不會,畢竟作者數學不太好QQ。
除非有特殊的參考資料,否則本系列的文大概都不會附,畢竟大部分的內容都會是從英文維基跟haskell的原始碼生出來的。
希望我不要棄坑。


  1. 若xs中的每個元素都滿足p,則all p xs回傳True,否則回傳False。其中xs是一個串列,p是一個謂語。

  2. 事實上並非如此,真正的型別會是 (+) :: Num a => a -> a -> a,不過為了說明方便就先假設是文中的樣子了

  3. 在Haskell中(->)是右結合的,因此括號可以省略