用 48 小時編寫你自己的 Scheme/第一步
首先,你需要安裝 GHC。在 GNU/Linux 上,它通常是預安裝的,或者可以透過包管理器(例如 apt 或 yum 或 pacman,具體取決於你的發行版)獲得。它也可以從 http://www.haskell.org/ghc/ 下載。二進位制包可能是最簡單的,除非你真的知道自己在做什麼。它應該像任何其他軟體包一樣下載和安裝。本教程是在 GNU/Linux 上開發的,但只要你瞭解如何使用命令列,所有內容也應該可以在 Windows 上執行,或者在 Macintosh 上從終端中執行。
對於 UNIX(或 Windows Emacs)使用者,有一個非常好的 Emacs 模式,包括語法高亮和自動縮排。Windows 使用者可以使用記事本或任何其他文字編輯器:Haskell 語法相當適合使用記事本,但你必須注意縮排。 Eclipse 使用者可能想嘗試使用 eclipsefp 外掛。
現在,是時候編寫你的第一個 Haskell 程式了。這個程式將從命令列讀取一個名稱,然後列印一個問候語。建立一個以 ".hs" 結尾的檔案,並輸入以下文字。確保縮排正確,否則可能無法編譯。
module Main where
import System.Environment
main :: IO ()
main = do
args <- getArgs
putStrLn ("Hello, " ++ args !! 0)
讓我們來看一下這段程式碼。前兩行指定我們將建立一個名為 Main 的模組,並匯入 System 模組。每個 Haskell 程式都從一個名為 main 的操作開始,該操作位於名為 Main 的模組中。該模組可以匯入其他模組,但必須存在才能使編譯器生成可執行檔案。Haskell 區分大小寫:模組名稱始終大寫,定義始終小寫。
行 main :: IO () 是一個型別宣告:它說明 main 的型別是 IO (),這是一個帶有單位型別 () 值的 IO 操作。單位型別只允許一個值,也表示為 (),因此不包含任何資訊。Haskell 中的型別宣告是可選的:編譯器會自動推斷它們,並且只會在它們與你指定的型別不同時才會報錯。在本教程中,我會明確指定所有宣告的型別,以提高畫質晰度。如果你在家跟著學習,你可能想省略它們,因為在構建程式時,需要更改的內容更少。
IO 型別是 Monad 類(一種型別類)的例項。Monad 是一個概念。說一個值是 Monad 類中的一種型別,就是說
- 此值附帶(某種型別的)額外資訊;
- 大多數函式不需要關心這些資訊。
在本例中,
- "額外資訊" 是要使用隨附的值執行的 IO 操作;
- 而基本值(附帶資訊)是空值,表示為
()。
IO [String] 和 IO () 都屬於同一個 IO Monad 型別,但它們具有不同的基本型別。它們作用於(並傳遞)不同型別的值,[String] 和 ()。
"附帶(隱藏)資訊的" 值稱為 "Monadic 值"。
"Monadic 值" 通常被稱為 "操作",因為考慮使用 IO Monad 的最簡單方法是考慮影響外部世界的一系列操作。這些操作序列可能傳遞基本值,並且每個操作都能夠作用於這些值。
Haskell 是一種函式式語言:你不會像其他語言那樣提供一系列指令給計算機執行,而是提供一組定義,告訴它如何執行它可能需要的每個函式。這些定義使用各種操作和函式的組合。編譯器會找出將所有內容組合在一起的執行路徑。
要編寫這些定義中的一個,你將其設定為一個等式。左側定義一個名稱,以及可選的 模式(將在稍後 解釋),這些模式將繫結變數。右側定義其他定義的一些組合,告訴計算機在遇到該名稱時該做什麼。這些等式就像代數中的普通等式一樣:你始終可以在程式的文字中將右側替換為左側,它將計算出相同的值。這種被稱為 "引用透明度" 的特性,使得對 Haskell 程式的推理比其他語言容易得多。
我們如何定義 main 操作?我們知道它必須是一個 IO () 操作,我們希望它讀取命令列引數並列印一些輸出,最終產生 () 或無價值。
建立 IO 操作有兩種方法(直接建立或透過呼叫執行它們的函式)
- 使用
return函式將普通值提升到 IO Monad 中。 - 組合兩個現有的 IO 操作。
由於我們想做兩件事,我們將採用第二種方法。內建操作 getArgs 讀取命令列引數並將它們作為字串列表傳遞。內建函式 putStrLn 接受一個字串並建立一個將該字串寫入控制檯的操作。
要組合這些操作,我們使用 do 塊。do 塊由一系列行組成,所有這些行都與 do 之後第一個非空格字元對齊。每行可以採用兩種形式之一
name <- action1action2
第一種形式將 action1 的結果繫結到 name,以便在接下來的操作中可用。例如,如果 action1 的型別是 IO [String](一個返回字串列表的 IO 操作,如 getArgs),那麼 name 將在所有後續的 actions 中繫結到透過使用 "繫結" 運算子 >>= 傳遞的字串列表。第二種形式只執行 action2,透過 >> 運算子將其與下一行(如果有)進行排序。繫結運算子在每個 Monad 中具有不同的語義:在 IO Monad 的情況下,它會順序執行操作,執行 actions 導致的任何外部副作用。由於這種組合的語義取決於所使用的特定 Monad,因此你不能在同一個 do 塊中混合不同 Monad 型別的操作 - 只能使用 IO Monad(它們都在同一個 "管道" 中)。
當然,這些 actions 本身可能呼叫函式或複雜的表示式,並傳遞它們的結果(透過呼叫 return 函式或其他最終執行此操作的函式)。在本例中,我們首先獲取引數列表中的第一個元素(索引為 0,args !! 0),將其附加到字串 "Hello, " 的末尾("Hello, " ++),最後將其傳遞給 putStrLn,它會建立一個新的 IO 操作,參與 do 塊排序。
這樣建立的新操作(如上所述,是操作的組合序列)儲存在型別為 IO () 的識別符號 main 中。Haskell 系統注意到這個定義,並執行其中的操作。
字串是 Haskell 中的字元列表,因此你可以在它們上使用任何列表函式和運算子。標準運算子及其優先順序的完整表如下
| 運算子 | 優先順序 | 結合性 | 描述 |
|---|---|---|---|
.
|
9 | 右 | 函式組合 |
!!
|
左 | 列表索引 | |
^, ^^, **
|
8 | 右 | 求冪(整數、分數和浮點數) |
*, /
|
7 | 左 | 乘法、除法 |
+, -
|
6 | 左 | 加法、減法 |
:
|
5 | 右 | cons(列表構建) |
++
|
右 | 列表連線 | |
`elem`, `notElem` |
4 | 左 | 列表成員資格 |
==, /=, <, <=, >=, >
|
左 | 相等、不等和其他關係運算符 | |
&&
|
3 | 右 | 邏輯與 |
||
|
2 | 右 | 邏輯或 |
>>, >>=
|
1 | 左 | 忽略返回值的 Monadic 繫結,將值傳遞給下一個函式的 Monadic 繫結 |
=<<
|
右 | 反向 Monadic 繫結(與上述相同,但引數顛倒) | |
$
|
0 | 右 | 中綴函式應用(f $ x 等同於 f x,但右結合而不是左結合) |
要編譯並執行該程式,請嘗試以下操作
debian:/home/jdtang/haskell_tutorial/code# ghc -o hello_you --make listing2.hs debian:/home/jdtang/haskell_tutorial/code# ./hello_you Jonathan Hello, Jonathan
-o 選項指定要建立的可執行檔案的名稱,然後你只需指定 Haskell 原始檔的名稱。
| 練習 |
|---|