Hello, Haskell

January 27, 2022

函數式程式設計(Functional programming)已經歷經時代的考驗,這年頭做為一個開發者,或多或少都有聽過函數式程式設計這個名詞,不少語言或程式庫中,已經出現函數式程式設計的基礎元素。

融入函數式的一些主流語言,本身通常是命令式(Imperative),而不是以函數式為主要典範,為了讓函數式元素在其本身中不至於過於突兀,這類元素多多少少都有經過一些調整;基於這類語言且具有函數式風格的程式庫,在函數式的相關概念或實現上,也都會有不少的調整。

這類調整是必要的,這也是函數式程式設計得以逐漸為開發者接受的主因之一,經過調整之後,才使得讓這類元素得以成為開發者使用的選項之一。

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

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

安裝 GHC

那麼,就不閒話了,來看看如何用 Haskell 寫個「哈囉!世界!」吧!首先要有 Haskell 編譯器,這邊使用的是 GHC(Glasgow Haskell Compiler),撰寫這份文件使用的是 9.2.1 的 Windows 版本

下載後解壓縮,在 PATH 中增加 bin 目錄的路徑,就可以直接使用了,你可以先進入 ghci 環境,這是 Haskell 的 REPL(read–eval–print loop)環境,先來幾個簡單的指令:

>ghci
GHCi, version 9.2.1: https://www.haskell.org/ghc/  :? for help
ghci> 1 + 2
3
ghci> 10 / 3
3.3333333333333335
ghci>  5 + "10"

<interactive>:3:4: error:
    ‧ No instance for (Num String) arising from a use of ‘+’
    ‧ In the expression: 5 + "10"
      In an equation for ‘it’: it = 5 + "10"
ghci> :q
Leaving GHCi.

>

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

>ghci
GHCi, version 9.2.1: https://www.haskell.org/ghc/  :? for help
ghci> :?
 Commands available from the prompt:

   <statement>                 evaluate/run <statement>
   :                           repeat last command
   :{\n ..lines.. \n:}\n       multiline command
   :add [*]<module> ...        add module(s) to the current target set
   :browse[!] [[*]<mod>]       display the names defined by module <mod>
                               (!: more details; *: all top-level names)
   :cd <dir>                   change directory to <dir>
   :cmd <expr>                 run the commands returned by <expr>::IO String
   :complete <dom> [<rng>] <s> list completions for partial input string
   :ctags[!] [<file>]          create tags file <file> for Vi (default: "tags")
                               (!: use regex instead of line number)
   :def[!] <cmd> <expr>        define command :<cmd> (later defined command has
                               precedence, ::<cmd> is always a builtin command)
                               (!: redefine an existing command name)
   :doc <name>                 display docs for the given name (experimental)
   :edit <file>                edit file
   :edit                       edit last module
   :etags [<file>]             create tags file <file> for Emacs (default: "TAGS")
   :help, :?                   display this list of commands
   :info[!] [<name> ...]       display information about the given names
                               (!: do not filter instances)
   :instances <type>           display the class instances available for <type>
   :issafe [<mod>]             display safe haskell information of module <mod>
   :kind[!] <type>             show the kind of <type>
                               (!: also print the normalised type)
   :load[!] [*]<module> ...    load module(s) and their dependents
                               (!: defer type errors)
   :main [<arguments> ...]     run the main function with the given arguments
   :module [+/-] [*]<mod> ...  set the context for expression evaluation

    ...略
ghci>

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

ghci> :set prompt haskell>
haskell> 1 + 1
2
haskell> :q
Leaving GHCi.

>

哈囉!世界!

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

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

main = putStrLn "哈囉!世界!"

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

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

>ghc hello.hs
[1 of 1] Compiling Main             ( hello.hs, hello.o )
Linking hello.exe ...

>hello
哈囉!世界!

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

互動版「哈囉!世界!」

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

請輸入你的名稱:

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

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

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

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

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

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

編譯時會看到以下錯誤訊息,告訴你 do 區塊是有問題的:

>ghc hello.hs
[1 of 1] Compiling Main             ( hello.hs, hello.o )

hello.hs:1:8: error:
    Unexpected do block in function application:
        do putStrLn "請輸入你的名稱:"
    You could write it with parentheses
    Or perhaps you meant to enable BlockArguments?
  |
1 | main = do putStrLn "請輸入你的名稱:"
  |        ^^^^^^^^^^^^^^^^^^^^^^

hello.hs:2:10: error:
    parse error on input ‘<-’
    Perhaps this statement should be within a 'do' block?
  |
2 |     name <- getLine
  |          ^^

接著可以看到 ++,這可以用來串接字串,++ 實際上是個函式,函式在 Haskell 是無所不在的,像 +-*/ 運算都是函式。

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

>hello
請輸入你的名稱:
Justin
哈囉!Justin!

嗯?提示輸入名稱可以不換行嗎?可以是可以,可以將 putStrLn 改為 putStr 就不會換行了:

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

不過編譯後執行時反而更怪了:

>hello
Justin
請輸入你的名稱:哈囉!Justin!

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

import System.IO

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

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

>hello
請輸入你的名稱:Justin
哈囉!Justin!

更多細節

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

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

至於 <-,嗯!從 IO Monad 中取出東西然後指定給 name!Monad?一聽就很嚇人,如果你在命令式語言中,曾經使用過 flatMap 之類的 API,其實就有用過 Monad 概念的經驗了,如果想從命令式語言的觀點,來試著認識一下,可以參考〈FlatMap 〉。

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

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

分享到 LinkedIn 分享到 Facebook 分享到 Twitter