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
一步一步來吧!之後的文章看過之後,你就慢慢會瞭解的!