跳轉到內容

Haskell/Preliminaries

來自華夏公益教科書

注意

此頁面不是 Haskell 華夏公益教科書的主要部分。它與主要介紹性材料有重疊,但沒有那麼完善。它對指令式程式設計背景有一些假設。因為學習在從多個角度體驗事物時效果最好,所以將此作為補充材料來強化基本概念,可能比簡單地重新閱讀其他章節更有效。不過,此頁面基本上是多餘的。


(本節中的所有示例都可以鍵入到 Haskell 原始檔中,並透過將該檔案載入到 GHC 或 Hugs 中來進行評估。)

不改變的變數和改變的函式

[編輯 | 編輯原始碼]

函數語言程式設計背後的主要思想之一是引用透明性——即函式和引數始終可以被函式的返回值替換。例如,表示式2+2可以與值4互換。這有利於 Haskell 中的一些重要概念,包括惰性求值和能夠重新排序表示式以併發執行它們的能力。

與 C++ 等命令式語言不同,Haskell 不將變數視為記憶體位置。在 Haskell 中,變數繫結到其存在的任何範圍內特定值。在大多數情況下,它們更像是常量而不是變數。變數在數學中的概念實際上與在 Haskell 中相同。在以下示例中,x 繫結到 3;它不可能改變。

x = 6 / 2

如果你習慣了指令式程式設計,這可能看起來非常奇怪。如何在沒有實際變數的情況下編寫有用的程式?好吧,Haskell 的函式引數可以起到相同的目的:一旦你將值傳遞給函式,該函式就可以在任何地方使用它。要更改引數的值,只需再次呼叫該函式即可。所以答案是遞迴。

以下是一些 Haskell 宣告

x = 3
y = x * 2

你可能已經猜到,最後 y = 6。看起來很像數學,對吧?

但是,與傳統語言不同,順序無關緊要。Haskell 沒有“控制流”的概念,因此它不會從頂部開始執行並一直執行到最後。所以這意味著與上面的完全相同

y = x * 2
x = 3

你當然也可以定義函式。這就是生活開始變得有趣的地方。

double x = x * 2
z = double y

這表示函式“double”接受一個引數“x”,結果等於 x 的兩倍。注意缺少括號:在 Haskell 中,“double y”表示將“double”函式應用於值“y”。括號只用於確定求值優先順序,就像數學中一樣。

到目前為止,這讓你能夠完成電子表格所能做的事情:這個世界中的變數看起來非常像電子表格中的單元格,你可以設定函式來“處理”這些變數。

現在看看這個函式

factorial 0 = 1
factorial n = n * factorial (n-1)

這定義了一個名為“factorial”的新函式。第一行表示 0 的階乘為 1,第二行表示任何其他數字“n”的階乘等於“n”乘以“n-1”的階乘。注意“n-1”周圍的括號:如果沒有這些括號,它將被解析為“(factorial n) - 1”;函式應用(如這個過程所知)具有比加法或求冪等任何正則運算子更高的優先順序。

這顯示瞭如何在 Haskell 中進行“迴圈”。你沒有像“n = n - 1”這樣的迴圈計數器,因為這會改變“n”的值。因此,你使用遞迴。

與我們之前的示例不同,兩個遞迴宣告的順序很重要。Haskell 從頂部開始匹配函式呼叫,並選擇第一個匹配的函式。因此,如果兩個宣告顛倒,則編譯器將得出結論,階乘 0 等於 0 * 階乘 -1,依此類推,直到無窮大。這不是我們想要的。

尾遞迴

[編輯 | 編輯原始碼]

如果你想知道堆疊空間,所有這些遞迴最終會不會導致堆疊溢位?答案是 Haskell 支援尾遞迴(每個體面的函式式語言的編譯器都支援)。尾遞迴的思想是,如果函式最後做的事情是呼叫自身,那麼直譯器不妨不必將東西放到堆疊中,而只需使用函式的新引數“跳轉”到開頭。不過,在這種情況下,階乘函式必須用“(n-1)”呼叫自身,然後將結果乘以“n”。

因此,我們的階乘函式不是尾遞迴的(因為它必須在呼叫自身後將結果乘以 n)。但是,這個函式是尾遞迴的

factorial n = factorial' n 1

factorial' 0 a = a
factorial' n a = factorial' (n-1) (n*a)

這裡有兩個函式。它們的名字幾乎相同,除了第二個函式有一個撇號。(Haskell 識別符號允許包含撇號。)按照慣例,撇號用於表示某事物的變體。

這裡,factorial'函式與以前一樣,接受一個引數n,但它還接受一個第二個“累加器”引數。由於呼叫自身是它做的最後一件事,因此尾遞迴規則適用:不會將任何東西放到堆疊中;factorial'函式只是像它的引數始終是 n-1 和 n*a 一樣執行。

(注意這個引數與變數的相似性。引數在某種程度上是 Haskell 中的變數。)

使用此函式的程式設計師不想擔心累加器引數,因此使用普通階乘函式來“封裝”factorial',方法是提供累加器的初始值。

將階乘函式鍵入到 Haskell 原始檔中,並將其載入到您喜歡的 Haskell 環境中。

5 的階乘是多少?

1000 的階乘呢?這是你預期的結果嗎?

-1 的階乘呢?

你可能已經注意到上面有一些有趣的事情。你的引數或任何東西都沒有宣告為任何特定型別。

這是因為 Haskell 使用型別系統來推斷每個表示式的型別,即 Hindley-Milner 型別系統。但 Haskell 不是弱型別;它與 C++ 一樣是強型別,因此你不會遇到執行時型別錯誤。任何編譯的程式都保證不會出現型別錯誤。另一個好處是,當你犯錯誤時,它幾乎總會改變結果表示式的型別,並且編譯器會將其標記為錯誤。(你的程式 simply won't compile。)你可能仍然希望出於一些原因為你的函式編寫型別。

  1. 它有助於記錄你的程式。
  2. 當你犯錯時,型別宣告有助於縮小問題的根源。
  3. 最重要的是,它將幫助你學習。理解型別系統是掌握 Haskell 的基礎。

型別宣告的語法如下

x :: Integer
factorial :: Integer -> Integer

你應該將第一個宣告理解為“x 的型別為 Integer”或“x 具有型別 Integer”。'::' 告訴我們我們指的是一個型別。你會注意到階乘看起來有點不同。它是一個接受 Integer 並返回 Integer 的函式。稍後你會明白為什麼我們在描述型別時使用 ->,以及為什麼描述函式型別在語法上與描述變數型別如此接近。

最後一點:以大寫字母開頭的識別符號保留用於型別、模組和資料建構函式(有點像名稱空間,現在不用擔心)。如果你嘗試宣告一個以大寫字母開頭的變數或函式,編譯器會報錯。

華夏公益教科書