Haskell 基礎語法

2026-03-17 16:36:4021min

開始一個新專案

使用 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

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 的參數是什麼?回傳值是什麼?

do 語法、型別與 IO monad

上方的 run 的例子中,看起來好像跟一般指令式語言一樣。其實這其中包函了一個語法糖: do notation

Haskell 最廣為人知的就是它的 Monad 。之後會學到 typeclassMonad,對 do 會有更精確的理解 。現在,我們先暫時使用簡化的比喻說明。以下說明不精準,但現階段可以幫助理解 do 的行為。

Haskell 有方式可以標注函式是有 IO 副作用的: runmain 函式都回傳 IO () ,表示 該函式有 IO 操作。

IO Monad 比喻:型別就像盒子

一個型別可以先簡單的把它想像成一層一層的盒子。以上例來說 IO () 是一個盒子,它使用 IO 這個上下文(Context),並最終回傳一個 ()

使用盒子和上下文來想像 IO 是個非常簡化的理解,但在初學的時,對於理解很有幫助。

do 語法

Haskell 是一個純函式語言。換句話說,它的程式碼的所有操作都是函式的組合。既然如此,為什麼 run 函式這麼像是一般的指令語言?

do 語法有一個很重要的特性,它將函式組合用類似指令式的方式展開。

Haskell 內要將函式組合起來,有一個很重要的原則:它們必須在同一個「上下文」中(例如 IO)。

我們使用 GHCi 來觀察一下 run 使用到的函式

ghci> :t putStrLn
putStrLn :: String -> IO ()
ghci> :t getLine
getLine :: IO String

putStrLngetLine 兩個函式的回傳值都是 IO Context。因此,在 run 這個型別為 IO () 函式中,可以透過 do 來組合這些 IO 函式。

可以這樣理解:

  • IO 是一種「上下文(盒子)」
  • do 讓我們可以把多個在同一個上下文中的操作串接起來

<- syntax

getLine 函式使用了 <- ,可以把它想像成一個從盒子內取出內部值的操作。

getLine 的回傳型別: IO String 。使用 <- ,可以將 IO 盒子內的 String 拿出來。

在寫 Haskell 的函式時,在使用 do 的函式內,可以寫很多很多的函式操作, 只要該函式的最後一個表達式和 run 的型別相同就好。如 run 宣告為 IO (),所以只要 do 內的最後一個表達示型別為 IO () 即可。

可以在 do 內使用回傳值不是 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 

補充:

  1. $ 是函式應用運算子,在 $ 右方所有函式都會先被應用後,再應用到左方函式。 如 putStrLn $ "Hello, Haskell!" <> "Hello, " <> line ,會先將右方的函式全部串接好,在將 putStrLn 應用在該 String 上。
  2. <> 是串接運算子,可以用來串接 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 ()

Haskell 是個強型別語言

強型別指的是:當使用者定義了一個型別後,它就是這個型別,不會自動做隱式型別轉換。在程式語言中,避免隱式的型別轉換可以減少許多不預期的 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 難學的地方在於,它的所有概念都要學過後,才有辦法開始正常打程式,否則會覺得寫什麼都綁手綁腳的。

筆者會盡量的讓文章有漸進式,讓讀者可以慢慢的從簡單的實作開始,到最終使用它來寫一般程式。

留言