跳轉至內容

Haskell/入門

來自 Wikibooks,開放世界的開放書籍

注意

此頁面不是 Haskell Wikibook 的主要部分。它與主要的入門材料有重疊。因為從多個角度體驗事物是學習的最佳方式,所以閱讀本文作為補充以加強基本概念可能會有用。

此頁面在解釋計算和程式設計的基礎知識方面尤其詳盡,並且還闡明瞭關於 Haskell 的一些精確細節。將來可能會將此頁面中的一些元素合併回主書中。


Haskell 是一種程式語言。如果您能夠編寫和理解 Haskell,則可以建立新的計算機程式,並理解和修改其他人編寫的程式。學習程式設計是一項相當複雜的任務,但 Haskell 是一種很好的入門方式,因為它相當簡單且可預測。即使您最終用其他語言進行大部分程式設計,您的大部分知識也將得以保留。然而,Haskell 不僅僅是一種入門語言;它也是最先進和功能強大的語言之一。

Haskell 軟體

[編輯 | 編輯原始碼]

要開始使用任何程式語言,您將需要一些特殊的軟體來構成您的開發工具鏈。至少,您需要一個編譯器或一個直譯器

首先,我們需要揭示一些關於計算機如何工作的知識。您可能聽說過 CPU(中央處理器)。它們是硬體部件,負責解釋儲存在計算機記憶體中的稱為機器語言的資料。機器語言編碼簡單的指令,當由 CPU 處理時,會導致計算機執行有用的操作,例如將您帶到此網頁。換句話說,它是一種程式語言。您的計算機執行的程式及其操作的資料以相同的方式儲存。

這種架構的一個重要結果是程式可以編寫其他程式。這就是直譯器和編譯器的工作原理。它們將用 Haskell 等語言編寫的程式翻譯成用機器語言編寫的程式,然後計算機可以直接執行這些程式。

編譯器和直譯器之間的區別在於軟體的內部工作原理,您不必過多擔心它。如今,這種區別變得越來越模糊和無關緊要。[1]

對於 Haskell,我們使用Glasgow Haskell 編譯器 (GHC)。它是一個免費/自由/開源程式,適用於所有主要作業系統。下載

REPL:使用 Haskell 作為計算器

[編輯 | 編輯原始碼]

GHC 包含一個稱為 GHCi 或“GHC 互動式”的程式。此程式允許您在一行中鍵入小的 Haskell 程式,並在您按下 Enter 鍵時執行它們。請查閱GHC 文件以獲取有關如何啟動 GHCi 的資訊,並執行此操作。

您應該看到一個提示符,例如 Main>,出現在您面前。(如果它顯示其他內容,例如 Prelude>,請不要驚慌)。當 GHCi 處於此狀態時,它已準備好接受並執行程式。嘗試鍵入以下簡單程式

1 + 1

GHCi 應該透過顯示數字 2 來響應。實際上,這就是此程式的目的:計算 1 和 1 的和,並顯示它。計算事物並顯示它們的程式稱為表示式,它們構成了 Haskell 的大部分內容。當您執行表示式以確定它產生的值時,它被稱為評估該表示式。

請注意,一旦您評估了表示式,您就可以做的不僅僅是顯示它;實際上,表示式產生的值有多種用途。我們將在後面遇到這些不同的技術。現在,只需意識到它們的存在即可。

回到不太理論的問題上,此時,您與 GHCi 的互動應該如下所示

Main> 1 + 1
2

在顯示示例時,我們也將以這種方式顯示與 GHCi 的互動。程式通常會與其結果一起包含,並在 Main> 提示符之前。您可以透過在提示符上鍵入程式來複制與 GHCi 相同的互動。

更多算術運算

[編輯 | 編輯原始碼]

您可能已經猜到,Haskell 支援全套算術運算子;加法 (+)、減法 (-)、乘法 (*) 和除法 (/)。數字可以用通常的方式表示為整數或實數,但有一個例外;任何數字都不能在小數點前不寫數字。例如,.5 應始終寫為 0.5。運算子遵循正常的運算順序,並且可以像往常一樣使用括號。您還可以使用各種常量和函式,例如 pisqrtlogsincostan

繼續嘗試一些表示式;如果您弄錯了,您不會破壞任何東西。以下是一些簡單的示例

Main> 3 * 3 * 3
27
Main> 9.6
9.6
Main> 5 / 7
0.7142857142857143
Main> sqrt(2)
1.4142135623730951
Main> (7 - 5) * 3
6
Main> cos(pi)
-1.0

請注意,示例中的空格不是必需的;第一個可以寫成 3*3*3,第五個可以寫成 (7-5)*3,等等。通常,Haskell 對空格並不挑剔,除非另有說明。但是,如有疑問,請使用更多空格,而不是更少空格。

任何計算機使用者都熟悉錯誤。您在程式設計時也會遇到錯誤。嘗試一下這個小實驗

Main> 1 +
<interactive>:1:3: parse error (possibly incorrect indentation)

當您在程式中犯錯誤時,GHCi 會通知您,並告訴您它認為存在的問題。不幸的是,它並不總是猜對,並且它通常對其診斷的描述相當隱晦。在此示例中,除了關於縮排的無用註釋外,它是在正確的軌道上,但它仍然存在後一個問題,即描述相當隱晦。那麼,“解析錯誤”是什麼呢?

一個解析器是編譯器的一部分,負責將程式分解成邏輯塊,並將其轉換為適合轉換為機器語言的格式。“解析錯誤”是指編譯器無法理解您的程式;這意味著您的程式存在問題。

那麼,<interactive>:1:3: 是什麼意思呢?第一部分,<interactive>,表示它正在報告您剛剛鍵入的程式的錯誤。[2] 第一個數字是發生錯誤的行號;對於互動式輸入的程式,它始終為 1,因此,同樣,這對我們來說沒有太大用處。第二個是 GHC 認為錯誤程式碼段所在的列(實際上,第一列編號為零,因此它是違規程式碼的列號減 1)。這裡它指向第四列,緊跟在加號之後;這是正確的,因為問題是我們省略了運算子的右運算元。(順便說一句,一般來說,不要將這些數字視為金科玉律;GHC 有時會給您稍微偏離的數字。)

現在,GHC 猜錯問題時會發生什麼情況?讓我們考慮一下上面錯誤的一個稍微修改後的版本

Main> (1 +)

Top level:
    No instance for (Show (a -> a))
      arising from use of `print' at Top level
    Probable fix: add an instance declaration for (Show (a -> a))
    In a 'do' expression: print it

我們做了一個小改動,錯誤訊息完全改變了。此外,該訊息現在診斷的問題與實際問題完全不同,並且提到了您尚未學習的 Haskell 的相當高階的功能。不幸的是,一大類錯誤會產生這樣的訊息;處理這些訊息是學習 Haskell 的一個更具挑戰性的方面。(其他程式設計系統也存在此問題,但大多數比 Haskell 好一些。)

當然,這是一個極端的例子;大多數情況下,訊息不會像這個例子這樣偏離目標。

嘗試鍵入一些無效的程式,看看您會收到什麼型別的錯誤訊息,以便對它們有所瞭解。再次強調,您不會破壞任何東西,所以不用擔心。以下是一些示例

Main> xxx
<interactive>:1:0: Not in scope: `xxx'
Main> .5
<interactive>:1:0: parse error on input `.'
Main> 5*.5
<interactive>:1:1: Not in scope: `*.'
Main> &$#^!
<interactive>:1:0: parse error on input `&$#^!'
Main> sqrt sqrt 4
<interactive>:1:0:
    No instance for (Floating (a -> a))
      arising from use of `sqrt' at <interactive>:1:0-3
    Probable fix: add an instance declaration for (Floating (a -> a))
    In the definition of `it': it = sqrt sqrt 4
Main> sqrt(sqrt(4))
1.4142135623730951

Haskell 支援稱為變數的功能。這些類似於代數中的變數,但對它們的用法有更多限制。

變數由一個或多個字母命名;xjanerespondxyzzy都是變數的有效名稱。它們也可以包含大寫字母,但不能作為第一個字母;例如,Jane不是有效的變數名,而jAnE是。大寫字母的典型用法是分隔名稱中的單詞;例如,通常編寫multiWordVariableName而不是編寫multiwordvariablename,這樣更容易閱讀。

您可以使用以下形式的程式為變數賦值

 let variable name = value

例如,程式let x = 5x定義為五。值也可以是任意表達式,例如(2 + 3) / 1

請注意,變數名是唯一可以出現在等號左側的內容。以下程式都無法正確工作

Main> let 5 = x
Main> let 2 * x = 5
Main> let x = x

奇怪的是,GHCi 對後兩個程式沒有生成任何錯誤。這是因為它們是有效的 Haskell 程式碼;只是沒有達到您的預期效果。例如,在執行第二個語句後,2 * 7 的值為五,並且,在執行第三個語句後,嘗試查詢x的值會導致 GHCi 掛起。(在 UNIX 中按 ctrl+c,在 Mac OS X 中按 ctrl+.,在 Windows 中按 ctrl+break 以停止它。)

底線:Haskell 不是嚴格意義上的代數;不要將其當作代數來使用。

一旦您確定了變數的值,您就可以在後續表示式中使用該變數;變數的每次出現都將被替換為其值。例如

Main> let x = 5
Main> let y = 2 + 2
Main> let z = sqrt 9
Main> x * (y + z)
35.0

多個變數可以在一個let中使用分號分隔賦值來建立。例如,前面的示例可以改寫為

Main> let x = 5; y = 2 + 2; z = sqrt 9
Main> x * (y + z)
35.0

雖然您已經掌握了相當多的內容,但您可能覺得離用計算機程式設計的目標還很遠。我們到目前為止編寫的程式並沒有完成很多工作;您用紙和筆或傳統的袖珍計算器也能做到同樣的事情。在本節結束時,您將能夠做一些不太瑣碎的事情,但示例仍然會非常人為。但是,此時,您別無選擇,只能向上發展;您可以編寫的程式的多樣性和複雜性將從這裡開始呈指數級增長,並在接下來的幾個章節中持續增長,因為您將學習新的表示式型別以及組合表示式的新方法。

Haskell 支援函式。首先,不要假設這個詞與數學函式相關的任何先入為主的觀念;Haskell 的函式有點不同。

與 Haskell 函式更匹配的是 Haskell 變數。變數代表一個表示式。函式也代表一個表示式,但有一個變化:它接受一個引數;一個在函式內部定義的變數,在使用函式時提供。這個概念也許可以透過示例來最好地理解

Main> let f x = x + 3

此程式碼定義了函式f,其引數為x

我們能用f做什麼?我們可以將其應用於一個引數。假設引數為 4。此程式碼為

Main> f 4
7

這裡發生了什麼?讓我們回顧一下f的定義

let f x = x + 3

表示式f 4f的定義替換,其中x被替換為四。因此,它變成了4 + 3,當然,結果是 7。

函式定義的一般形式為

 let function argument = definition

函式應用的一般形式為

 function argument

請注意,在這些定義中,“引數”一詞以兩種不同的方式使用。第一種是函式與其定義相關聯的型別;只是一個代表值的名稱,在應用函式時提供。第二種是在應用函式時取代先前型別引數的實際值,從而產生可以計算的表示式。

再舉幾個例子

Main> let reciprocal n = 1 / n
Main> reciprocal 5
0.2
Main> let theSame thing = thing
Main> theSame 6
6
Main> let funny joe = log(pi / joe) * cos(joe + 3)
Main> funny 6
0.5895282337509272

同樣,有一種簡單的技巧可以手動計算函式展開的值

  1. 寫下函式的展開式。
  2. 再次寫下它,將引數名稱的所有出現都替換為引數值。
  3. 計算結果表示式。

帶有多個引數的函式

[編輯 | 編輯原始碼]

函式表示法表明可以建立具有多個引數的函式。事實上,這是可能的,並且完全按照您的預期工作

Main> let add x y = x + y
Main> add 5 6
11
Main> let average x y z = (x + y + z) / 3
Main> average 5 6 7
6
Main> let first a b = a
Main> first 8 1
8

在使用多引數函式進行程式設計時,需要注意的一個“陷阱”是向函式傳遞過多引數或引數不足。以下示例演示了此問題

Main> average 1 2
Top level:
    No instance for (Show (a -> a))
      arising from use of `print' at Top level
    Probable fix: add an instance declaration for (Show (a -> a))
    In a 'do' expression: print it
Main> average 1 2 3 4
<interactive>:1:0:
    No instance for (Fractional (t -> a))
      arising from use of `average' at <interactive>:1:0-6
    Probable fix: add an instance declaration for (Fractional (t -> a))
    In the definition of `it': it = average 1 2 3 4

這兩個錯誤看起來非常相似。通常,如果您遇到此類錯誤,請檢查您是否向函式傳遞了正確數量的引數。

表示式中的函式

[編輯 | 編輯原始碼]

到目前為止,我們只將數字作為函式的引數。但是,您也可以將表示式作為引數,並將函式應用用作表示式

Main> let f x = 2 * x
Main> f (1 + 1)
4
Main> f (f (f 3))
24
Main> f 4 + 2
10

函式應用在運算子之前計算;因此,f 4 + 2等價於(f 4) + 2,而不是f (4 + 2).

由於函式應用是表示式,因此它們可以在函式定義中使用。例如

Main> let f x = 2 * x; g x = 3 + f x
Main> f 5
10
Main> g 5
13

這裡有幾點需要注意

  • 我們在let表示式中使用了分號表示法來壓縮兩個函式的定義;f x = 2 * xg x = 3 + f x.
  • fg之前定義,但不必如此;如果顛倒定義順序,程式仍然可以工作。但是,以下程式碼無法工作
Main> let g x = 3 + f x
<interactive>:1:14: Not in scope: `f'

(當 GHC 抱怨變數或函式“不在作用域內”時,它只是意味著它還沒有看到它的定義。如前所述,GHCi 要求在使用變數和函式之前對其進行定義。)

  • 的定義g, 3 + f x等價於3 + (f x),如前面給出的規則所規定。因此,g 5變為3 + (2 * 5)在展開gf的定義之後;該表示式進一步計算得到 13。

條件測試

[編輯 | 編輯原始碼]

我們承諾,隨著您閱讀本書的更多內容,您將學習編寫更新、更令人興奮的程式型別。在本節中,我們將填補拼圖中的另一塊缺失的部分。

您到目前為止編寫的程式似乎有些簡單;它們無法做出選擇,在不同時間做不同的事情。一種易於實現的簡單決策是使用if表示式。這些表示式的形式如下

 if test then expression else expression

測試部分可以採用以下形式之一

 expression == expression
 expression /= expression
 expression < expression
 expression > expression
 expression <= expression
 expression >= expression

每種形式描述了兩個表示式之間某種型別的關係;例如,<是數學上的小於測試。每個都是某個數學運算子的純文字形式

== 等於測試。它使用雙等號來區分它與在let表示式中使用的等號。
/= 不等於測試。它是的變體。
<, > 小於和大於測試。
<=, >= 小於等於和大於等於測試。是的變體。

當一個if表示式被計算時,如果測試部分宣告的內容為真(例如5 < 6),則它計算為then表示式;否則,如果測試部分宣告的內容為假(例如5 == 6),則它計算為else表示式。以下示例演示了這一點

Main> let x = 5
Main> if x < 7 then 1 else 2
1
Main> if x <= 5 then x + 1 else pi
6
Main> 1 + (if 2 * x == 10 then 2 else 1)
3
Main> if x < 6 then (if x < 5 then 1 else 2) else 3
2
Main> if x < 5 then 1 else (if x < 6 then 2 else 3)
2

請注意,在最後三個示例中,括號不是必需的;但是,如果最後一個示例是用加號運算子的運算元反轉來編寫的,則括號將是必需的;因此,它將是

(if 2 * x == 10 then 2 else 1) + 1

如果在沒有括號的情況下編寫它,它將計算為

Main> if 2 * x == 10 then 2 else 1 + 1
2

以下是一些使用if表示式中使用的等號。

絕對值

[編輯 | 編輯原始碼]
Main> let abs x = if x < 0 then -x else x
Main> abs 5
5
Main> abs (-3)
3
Main> abs 0
0

數值三路測試

[編輯 | 編輯原始碼]
Main> let nif x p z n = if x > 0 then p else if x == 0 then z else n
Main> nif 3 1 2 3
1
Main> nif 0 1 2 3
2
Main> nif (-6) 1 2 3
3

此函式對應於以下數學函式

最後的測試,而不是如果 x < 0 則 n,只是否則 n。這是因為,根據數值關係的定義,如果x既不大於也不等於零,則它必須大於零,因此測試將始終為真。此外,每個if都需要一個else子句,你會在永遠無法到達的else子句中放什麼?

實際上,Haskell 有一個用於這些情況的工具;undefined。這是某個特殊值的名稱,當作為表示式結果產生時,只需標記錯誤。它是在表示式“不可能”的情況下放置的良好值if表示式中使用的等號。

Main> let sign x = nif x 1 0 -1
Main> sign 5
1
Main> sign 0
0
Main> sign (-8)
-1

此函式對應於以下數學函式

我們可以這樣定義它

let sign x = if x > 0 then 1 else if x == 0 then 0 else -1

然而,由於nif結果是符號函式的推廣,將其定義為nif的應用更簡單。事實上,將nif擴充套件到函式體,您將得到與上述程式碼完全相同的程式碼!

邏輯連線

[編輯 | 編輯原始碼]

布林表示式和謂詞

[編輯 | 編輯原始碼]

參考文獻

[編輯 | 編輯原始碼]
  1. 例如:如果直譯器真正做的事情只是將表示式編譯成機器程式碼並非常快速地執行它,那麼直譯器是否為直譯器?鑑於w:部分求值,解釋和編譯之間的理論差異似乎最小或不存在。
  2. 還有其他程式輸入方法(我們現在忽略這些方法),這些方法將在此處顯示其他內容。
華夏公益教科書