使用 cabal init 開始一個新專案。我們來試試看一個簡單的 hello, world
程式。
module Main where
{-
This is first of my Haskell project for my blog post.
I'll try to demonstrate Haskell's basic syntax.
-}
-- This is a function
run :: IO ()
run = do
putStrLn $ "Please enter your name: "
line <- getLine
putStrLn $ "Hello, Haskell!" <> "Hello, " <> line
main :: IO ()
main = run
這是一個非常簡單的 hello, world 程式。一行一行的看程式碼的語法。
第二行開始 {- ... -} 是 Haskell 的多行注解語法。
第七行的 -- This is a function 是單行注解。Haskell
提供了兩種注解方式,兩種方式都可以使用。
雖然 Haskell 可以自動推斷型別,但是在寫 Haskell
時,習慣都會顯示的把函式型別寫出。 函式簽名在函式名稱及 ::
後,會定義函式的型別。
以上方的程式為例,run 這個函式,它會回傳一個 IO () 型別。
上方的範例中, run
是一個沒有任何參數的函式。如果要定義一個接受兩個參數的函式要怎麼寫呢?例如,要寫一個函式,接受兩個
Int 參數,並回傳之它的加總。
add :: Int -> Int -> Int
add a b = a + b
在 Haskell 的函式簽名中,參數都是用 ->
分隔的。最後一個型別是函式的回傳值。上方的例子,可以看做:接受兩個 Int
參數,並回傳一個 Int 參數的函式。
GHCi 是 Haskell 提供的一個 repl 工具。觀察一份新的 Haskell code 的時候,可以先使用 GHCi 讀取它。嘗試使用函式,並觀察函式的定義。
使用 cabal ,可以使用 cabal repl 進入互動模式。
cabal 預設會載入專案的module。也可以使用 :l <module> 指令讀取模組。
ghci> :l Main
[1 of 2] Compiling Main ( app/Main.hs, interpreted )
Ok, one module loaded.
GHCi 有個指令 :t,可以用來觀察函式或值的型別,非常實用。
ghci> :t add
add :: Int -> Int -> Int
不只自己定義的函式,也可以看使用到的模組的函式。例如上方使用到了
putStrLn ,試著觀察一下它的型別。
ghci> :t putStrLn
putStrLn :: String -> IO ()
練習看看,上方
putStrLn的參數是什麼?回傳值是什麼?
上方的 run 的例子中,看起來好像跟一般指令式語言一樣。其實這其中包函了一個語法糖:
do notation 。
Haskell 最廣為人知的就是它的 Monad 。之後會學到 typeclass
、Monad,對 do 會有更精確的理解 。現在,我們先暫時使用簡化的比喻說明。以下說明不精準,但現階段可以幫助理解 do 的行為。
Haskell 有方式可以標注函式是有 IO 副作用的: run 和 main 函式都回傳
IO () ,表示 該函式有 IO 操作。
一個型別可以先簡單的把它想像成一層一層的盒子。以上例來說 IO ()
是一個盒子,它使用 IO 這個上下文(Context),並最終回傳一個 () 。
使用盒子和上下文來想像 IO 是個非常簡化的理解,但在初學的時,對於理解很有幫助。
Haskell 是一個純函式語言。換句話說,它的程式碼的所有操作都是函式的組合。既然如此,為什麼 run 函式這麼像是一般的指令語言?
do 語法有一個很重要的特性,它將函式組合用類似指令式的方式展開。
Haskell 內要將函式組合起來,有一個很重要的原則:它們必須在同一個「上下文」中(例如 IO)。
我們使用 GHCi 來觀察一下 run 使用到的函式
ghci> :t putStrLn
putStrLn :: String -> IO ()
ghci> :t getLine
getLine :: IO String
putStrLn 和 getLine 兩個函式的回傳值都是 IO Context。因此,在 run
這個型別為 IO () 函式中,可以透過 do 來組合這些 IO 函式。
可以這樣理解:
getLine 函式使用了 <- ,可以把它想像成一個從盒子內取出內部值的操作。
getLine 的回傳型別: IO String 。使用 <- ,可以將 IO 盒子內的
String 拿出來。
在寫 Haskell 的函式時,在使用 do 的函式內,可以寫很多很多的函式操作, 只要該函式的最後一個表達式和 run 的型別相同就好。如 run 宣告為 IO (),所以只要 do 內的最後一個表達示型別為 IO () 即可。
IO 的函式嗎在目前已介紹的語法內,答案是:暫時還不行。do 是將相同上下文的函式串接在一起。如果你提供了不同的上下文,那函式是沒有辦法串接的。
在 do 內可以使用 let 綁定任意型別的值,未來提到 let 的時候會詳細說明。
我們改一下run範例,試著呼叫上方寫好的 add
run :: IO ()
run = do
putStrLn $ "Please enter your name: "
line <- getLine
putStrLn $ "Hello, Haskell!" <> "Hello, " <> line
add 4 3
補充:
$是函式應用運算子,在 $ 右方所有函式都會先被應用後,再應用到左方函式。 如putStrLn $ "Hello, Haskell!" <> "Hello, " <> line,會先將右方的函式全部串接好,在將putStrLn應用在該 String 上。<>是串接運算子,可以用來串接 String。後續章節會再介紹常用的函式。
嘗試編譯會看到以下 error
• Couldn't match expected type ‘IO ()’ with actual type ‘Int’
• In a stmt of a 'do' block: add 4 3
In the expression:
do putStrLn $ "Please enter your name: "
line <- getLine
putStrLn $ "Hello, Haskell!" <> "Hello, " <> line
add 4 3
In an equation for ‘run’:
run
= do putStrLn $ "Please enter your name: "
line <- getLine
putStrLn $ "Hello, Haskell!" <> "Hello, " <> line
....
|
17 | add 4 3
| ^^^^^^^
add 是一個純函式(回傳型別為 Int),不可以直接在 IO 上下文內使用,
所有在 run 的 do notation 下的函式,都應該是 IO 上下文的型別。
我們試著可以這樣改寫程式,讓程式通過編譯
run :: IO ()
run = do
putStrLn $ "Please enter your name: "
line <- getLine
putStrLn $ "Hello, Haskell!" <> "Hello, " <> line
putStrLn $ show $ add 4 3
在 GHCi 看一下 putStrLn $ show $ add 4 3 型別
ghci> :t putStrLn $ show $ add 4 3
putStrLn $ show $ add 4 3 :: IO ()
強型別指的是:當使用者定義了一個型別後,它就是這個型別,不會自動做隱式型別轉換。在程式語言中,避免隱式的型別轉換可以減少許多不預期的 bug 產生。
再回顧上方例子,用GHCi 觀察一下 putStrLn $ show $ add 4 3
內的函式的型別
ghci> :t add
add :: Int -> Int -> Int
ghci> :t putStrLn
putStrLn :: String -> IO ()
ghci> :t show
show :: Show a => a -> String
add 最終回傳的是一個 Int,所以它不能直接套用在 putStrLn 上,因為
putStrLn 是個接受 String 的函式。
Haskell 不會幫忙做 Int -> String 的轉換。
ghci> :t putStrLn $ add 4 3
<interactive>:1:12: error: [GHC-83865]
• Couldn't match type ‘Int’ with ‘[Char]’
Expected: String
Actual: Int
• In the second argument of ‘($)’, namely ‘add 4 3’
In the expression: putStrLn $ add 4 3
但,每個內建型別都有實作 show 函式,可以使用它將型別轉換成 String。
show 的函式簽名有些不一樣的東西。
show :: Show a => a -> String
a 是一個多型。 Show 是一個 typeclass
,後續再介紹。現階段可以理解成: 第一個參數可以回任何有實現 show
的型別。上面說過, 所有的內建型別都有實作 show,包含
Int,所以可以將它當作參數傳入 show。
今天簡單的介紹了基本的 Haskell 語法,包含怎麼使用 GHCi,do notation、型別的基本概念等等。
Haskell 難學的地方在於,它的所有概念都要學過後,才有辦法開始正常打程式,否則會覺得寫什麼都綁手綁腳的。
筆者會盡量的讓文章有漸進式,讓讀者可以慢慢的從簡單的實作開始,到最終使用它來寫一般程式。