跳轉到內容

用 48 小時編寫你自己的 Scheme/第一步

來自 Wikibooks,開放世界中的開放書籍
用 48 小時編寫你自己的 Scheme
第一步 解析 → 

首先,你需要安裝 GHC。在 GNU/Linux 上,它通常是預安裝的,或者可以透過包管理器(例如 aptyumpacman,具體取決於你的發行版)獲得。它也可以從 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 類中的一種型別,就是說

  1. 此值附帶(某種型別的)額外資訊;
  2. 大多數函式不需要關心這些資訊。

在本例中,

  1. "額外資訊" 是要使用隨附的值執行的 IO 操作;
  2. 而基本值(附帶資訊)是空值,表示為 ()

IO [String]IO () 都屬於同一個 IO Monad 型別,但它們具有不同的基本型別。它們作用於(並傳遞)不同型別的值,[String]()

"附帶(隱藏)資訊的" 值稱為 "Monadic 值"。

"Monadic 值" 通常被稱為 "操作",因為考慮使用 IO Monad 的最簡單方法是考慮影響外部世界的一系列操作。這些操作序列可能傳遞基本值,並且每個操作都能夠作用於這些值。

Haskell 是一種函式式語言:你不會像其他語言那樣提供一系列指令給計算機執行,而是提供一組定義,告訴它如何執行它可能需要的每個函式。這些定義使用各種操作和函式的組合。編譯器會找出將所有內容組合在一起的執行路徑。

要編寫這些定義中的一個,你將其設定為一個等式。左側定義一個名稱,以及可選的 模式(將在稍後 解釋),這些模式將繫結變數。右側定義其他定義的一些組合,告訴計算機在遇到該名稱時該做什麼。這些等式就像代數中的普通等式一樣:你始終可以在程式的文字中將右側替換為左側,它將計算出相同的值。這種被稱為 "引用透明度" 的特性,使得對 Haskell 程式的推理比其他語言容易得多。

我們如何定義 main 操作?我們知道它必須是一個 IO () 操作,我們希望它讀取命令列引數並列印一些輸出,最終產生 () 或無價值。

建立 IO 操作有兩種方法(直接建立或透過呼叫執行它們的函式)

  1. 使用 return 函式將普通值提升到 IO Monad 中。
  2. 組合兩個現有的 IO 操作。

由於我們想做兩件事,我們將採用第二種方法。內建操作 getArgs 讀取命令列引數並將它們作為字串列表傳遞。內建函式 putStrLn 接受一個字串並建立一個將該字串寫入控制檯的操作。

要組合這些操作,我們使用 do 塊。do 塊由一系列行組成,所有這些行都與 do 之後第一個非空格字元對齊。每行可以採用兩種形式之一

  1. name <- action1
  2. action2

第一種形式將 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 原始檔的名稱。

練習
  1. 更改程式,使其從命令列讀取 兩個 引數,並使用這兩個引數列印一條訊息
  2. 更改程式,使其對這兩個引數執行簡單的算術運算並列印結果。你可以使用 read 將字串轉換為數字,使用 show 將數字轉換回字串。嘗試不同的運算。
  3. getLine 是一個從控制檯讀取一行並將其作為字串返回的 IO 操作。更改程式,使其提示輸入姓名,讀取姓名,然後列印該姓名,而不是命令列值


用 48 小時編寫你自己的 Scheme
第一步 解析 → 
華夏公益教科書