Haskell Tutorial(2)一絲不苟的型態系統 << 前情
來寫個簡單的程式,可以接受使用者輸入整數,判斷其為奇數或偶數,下面這個範例是以〈Haskell Tutorial(1)哈囉!世界!〉中的範例為樣版略做擴充:
import System.IO
main = do
putStr "請輸入整數:"
hFlush stdout
input <- getLine
let number = read input::Int
result = if number `mod` 2 == 0 then "偶數" else "奇數"
putStrLn (input ++ "是" ++ result ++ "!")
使用 ghc 編譯,以下是個執行示範:

let 關鍵字
在這邊的新範例中,第一個看到新元素是 let 關鍵字,你可以用它來定義一個名稱,let number = 就字面意義上可以理解為「令 number 等於 …」,它的語法之一是 let ... in ... 。例如:

上圖中可以看到,「在 a + 20 中,你令 a 為 10 」,而 let 指定的名稱,只在 in 之中有效,在其他範圍中不可見。如果你省略了 in ,那 let 指定的名稱會在接下來的整個程式互動過程中都有效。
注意!我使用的描述是「let 指定的名稱」,而不是使用「let 指定的變數」,Haskell 中沒有主流程式語言中所謂的變數,當你寫下 let a = 10 ,表示 a 就是 10,不會再是什麼其他的東西,這像是數學中代數的概念,你可以令 x = 10,那麼 x 就是 10 了,不會代表別的!
在上例中,你也可以看到,let ... in ... 是個運算式,也就是說,它執行之後會傳回結果。
當你寫下 let a = 10 ,試圖指定 a 為其他的東西,就會引發編譯錯誤:

因此,主流程式語言中的變數在 Haskell 中是不存在的,你可以說 a 是個名稱,或說是個代數,令代數為某值之後就不可變(Immutable),是純函數式世界的明顯特徵之一。
別讓以下的過程讓你混淆了:

在上面的例子中,你的 let a = 10 省略了 in ,因此,a 在後續範圍中可見,當你重新使用 let a = 20 時,其實是建立了另一個範圍,而這個新的範圍中有個 a 為 20 ,跟前一個範圍的 a 沒有關係。
read 與 mod 函式
如果你在 GHCI 中使用 :t getLine ,這會告訴你 getLine 函式的型態為 getLine :: IO String ,簡單來說,這表示 <- 取出的值會是 String ,可是我們需要一個 Int 才能進行運算啊?你可以使用 read 函式,這可以將字串轉換為指定的型態,read input::Int 表示將 String 的 input 值轉換為 Int 值後傳回。
mod 函式是餘除函式,它會傳回兩數相除後的餘數,基本上你可以使用 mod 10 2 呼叫函式,表示要計算出 10 除以 2 的餘數,例如:

基本上,你也可以看到,Haskell 中呼叫函式並給定引數時,並不使用括號。不過,我們希望這類有兩個運算元的函式呼叫可以比較像數學式一些,這時你可以使用如上使用 ` 來括住函式。
if … then … else …
在其他程式語言中,會有 if..else 流程控制語法,在 Haskell 中是 if ... then ... else ... ,它是個運算式,也就是說它會有傳回值:

定義函式
一開始的範例,把所有程式碼都寫在 main 中,這樣並不好懂,使用函式來稍微整理一下會比較好:
import System.IO
prompt text = do
putStr text
hFlush stdout
descOddEven number =
if number `mod` 2 == 0 then "偶數" else "奇數"
main = do
prompt "請輸入整數:"
input <- getLine
let desc = descOddEven (read input::Int)
putStrLn (input ++ "是" ++ desc ++ "!")
如〈Haskell Tutorial(2)一絲不苟的型態系統〉中談過的,Haskell 具有強大的型態推斷能力,因而在這邊,你可以完全不用定義型態相關資訊。當然,如果你夠有信心,也可以指定函式應有的型態:
import System.IO
prompt :: String -> IO ()
prompt text = do
putStr text
hFlush stdout
descOddEven :: Int -> String
descOddEven number =
if number `mod` 2 == 0 then "偶數" else "奇數"
main = do
prompt "請輸入整數:"
input <- getLine
let desc = descOddEven (read input::Int)
putStrLn (input ++ "是" ++ desc ++ "!")
String -> IO () 表示 prompt 接受一個字串,傳回一個 IO () ,這是一個輸出操作的傳回結果,prompt 中有兩個輸出操作,因此用 do 將它們串成為一個大的輸出操作。Int -> String 表示,descOddEven 接受一個 Int ,傳回一個 String 。可以看到,Haskell 中任何函式之後都是接受 = ,也就是都會有傳回值,即使它是個輸入輸出操作,也會有傳回值,像是 IO () 。
注意!在函式中,參數也是不可變的,每當你呼叫函式時,你是令參數為指定的引數,在函式中,你無法改變參數的值,只能引用它的值。
先暫且不管那些有輸入輸出副作用(Side effect)的函式,像是 putStr 、getLine 或這邊自訂的 prompt 。
先將目光放在像 mod 、read 或這邊自訂的 descOddEven ,因為在這類函式中,你無法改變參數值,只能引用它的值,因此,只要指定的引數相同,無論呼叫多少次函式,結果都會是相同的,也就是這類函式沒有副作用,相同函式在相同引數下,可預期會有相同的結果,這樣的特性稱為引用透明性(Referential Transparency),這樣的透明性能使得你容易驗證函式的正確性。
透過適當的函式封裝了一些流程,現在 main 中的主流程就清楚多了:顯示提示文字、取得輸入、計算出奇偶描述字串、顯示格式化的結果。
當然,這篇的標題寫明了是「初探」,這表示在 Haskell 中,函式還有更多值得探討的地方 …
後續 >> Haskell Tutorial(4)這裏,那裏,到處都是函式
|