Haskell Tutorial(1)哈囉!世界!


函數式程式設計(Functional programming)已經歷經時代的考驗,這年頭做為一個開發者,或多或少都有聽過函數式程式設計這個名詞,不少主流語言中,也已經或逐步出現函數式程式設計的基礎元素,就連 Java 這個保守的語言,在 Java 8 中,除了 Lambda 語法本身具有一級函式(First class function)概念之外,也突然出現了不少函數式概念的 API。

這類主流語言中,不少本身並不是以函數式為主要典範(Paradigm),為了讓函數式元素在其本身中不至於過於突兀,這類元素多多少少都有經過一些調整,這類調整是必要的,這也是函數式程式設計得以逐漸為開發者接受的主因之一,經過調整之後,才使得讓這類元素得以成為開發者使用的選項之一。

然而,也正因為經過調整,在試圖從這類語言中探討函數式概念時,總有種朦朦朧朧看不清楚真貌的感覺,那麼,來學習一門純函數式語言如何?這就成了我想撰寫 Haskell Tutorial 一開始的動機。

實際上,已經有不少 Haskell 的好書,像是《Learn You a Haskell for Great Good!》,線上觀看是免費的,如果想購買電子書或實體書也行,中文翻譯為《Haskell 趣學指南》;其他書籍像是《Real World Haskell》也有線上版、電子書、實體書的選擇。

自己想來寫寫看,無非就是想整理一下這幾年的心得,用自己的順序來構築一個路徑。

安裝 GHC

那麼,就不閒話了,來看看如何用 Haskell 寫個「哈囉!世界!」吧!首先,安裝 Haskell 編譯器,這邊是在 Ubuntu Linux 下進行:

sudo apt-get install ghc

這邊使用的是 GHC(Glasgow Haskell Compiler),安裝好之後,實際上也會有個 GHCI,也就是直譯環境可以使用,直接輸入 ghci 指令就可以進入,先來幾個簡單的指令:

GHCI

在這邊基本上可以看到,Haskell 是強型別語言,數值 5 不能與字串 “10” 直接進行運算。要離開 GHCI,可以輸入 :q 或按下 Ctrl+D。實際上,在 GHCI 環境中時,你可以輸入 :? 來取得許多環境中可使用的指令說明,讓我們再度進入 GHCI:

GHCI 說明

來試著用 :set prompt 改一下 GHCI 中的提示文字:

set prompt

哈囉!世界!

一般來說,不少文件或書籍在介紹 Haskell 相關元素時,會有不少篇幅是使用 GHCI 來介紹的,因為這樣可以不用一開始接觸那麼多觀念,不過,我還是想先寫個原始碼、編譯、執行介紹,以便在一開始就能稍微瞭解這個流程。

那麼,該是來寫個「哈囉!世界!」的時候了,使用你慣用的編輯器,寫個 hello.hs:

main = putStrLn "哈囉!世界!"

原始碼記得用 UTF-8,這是 Ubuntu Linux 下預設的文字編碼,如果你要使用中文,這是最簡單的方式。Haskell 的程式進入點是 main 函式,putStrLn 會輸出指定文字之後換行,至於那個 =,在 Haskell 中,每個函式都必須有傳回值,= 表示putStrLn 的傳回值(IO (),現在不用太去管它)會指定給 main 作為其傳回值。

在存檔之後,使用 ghc 進行編譯:

GHC 編譯與執行

可以看到,編譯成功之後,會產生兩個檔案,.hi 是介面檔(interface file),包括了 hello 揭露(export)的函式訊息,.o 是目的碼(object code),執行之後,就可以看到「哈囉!世界!」。

互動版「哈囉!世界!」

還沒結束,通常我的第一個「哈囉!世界!」不會那麼簡單,至少要有個能與使用者互動的過程,那麼以下是第二個版本的「哈囉!世界!」:

請輸入你的名稱:

main = do
  putStrLn "請輸入你的名稱:"
  name <- getLine
  putStrLn ("哈囉!" ++ name ++ "!")

寫個能顯示中文、有基本互動的「哈囉!世界!」,會比在那比哪個程式語言的「哈囉!世界!」可以最短來得有意義,通常可以稍微揭露一門語言背後的複雜度。像是在這邊,就看到了幾個之後都還會詳加探討的元素。

先探討幾個簡單的元素,第一是縮排,Haskell 對縮排有嚴格的要求,同一層次的程式碼必須擁有相同縮排,這點與 Python 類似,但稍微寬鬆一些,例如,以下也是可以的:

main = do putStrLn "請輸入你的名稱:"
          name <- getLine
          putStrLn ("哈囉!" ++ name ++ "!")

因為 do 包括的區塊是位於同一層縮排,但以下就不行:

main = do putStrLn "請輸入你的名稱:"
    name <- getLine
    putStrLn ("哈囉!" ++ name ++ "!")

接下來可以看到 ++,這可以用來串接字串,實際上,之後會看到,++ 實際上是個函式,事實上,Haskell 中幾乎都是函式,像 +-*/ 運算都是函式。

在編譯與執行之後,你會看到以下結果:

互動版「哈囉!世界!」

嗯?提示輸入名稱可以不換行嗎?可以是可以,可以將 putStrLn 改為 putStr 就不會換行了,不過編譯後執行時反而更怪了:

互動版「哈囉!世界!」

提示文字怎麼會在輸入之後才出來,預設情況下,輸出會被緩衝,直到遇到換行符號,或者是緩衝區滿了才會輸出,你可以直接出清(flush)緩衝區:

import System.IO

main = do putStr "請輸入你的名稱:"
          hFlush stdout
          name <- getLine
          putStrLn ("哈囉!" ++ name ++ "!")

hFlush 位於 System.IO 模組,在這邊使用 import 將之匯入,這樣一來就順眼多了:

互動版「哈囉!世界!」

更多細節

以上的解釋,作為之後範例程式的基礎已經足夠,不過,我還有幾個細節沒解釋,例如 do<- 的作用,以下作個簡單解釋,不過有點複雜,你可以跳過沒關係,之後有機會就會解釋的!

在第一個「哈囉!世界!」中,putStrLn 會有 I/O 動作,I/O 動作實際執行是在被指定給 main 時,如果有多個 I/O 動作呢?你沒辦法將多個 I/O 動作同時指定給一個 main,那麼就把這些 I/O 動作串在一起,成為一個大的 I/O 動作,那就是 do 的作用。

至於 <-,嗯!從 IO Monad 中取出東西然後指定給 name!Monad?一聽就很嚇人,其實,Monad 就只是個模式,只是相對來說,這個模式比較不容易觀察與抽取出來,我在〈Java 8 Patterns〉中,試著用白話文以及 Java 來解釋過 Monad 模式,有興趣的話,可以看看!

至於 IO Monad 的作用,在 Haskell 中是作為無副作用(side effect)的解套工具 … 嗯!越來越遠了 … 喂喂喂!你還在嗎? … XD

一步一步來吧!之後的文章看過之後,你就慢慢會瞭解的!