Haskell Tutorial(3)初探代數與函式


來寫個簡單的程式,可以接受使用者輸入整數,判斷其為奇數或偶數,下面這個範例是以〈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 ...。例如:

let ... in ...

上圖中可以看到,「在 a + 20 中,你令 a10」,而 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),是純函數式世界的明顯特徵之一。

別讓以下的過程讓你混淆了:

GHCI 中的 let

在上面的例子中,你的 let a = 10 省略了 in,因此,a 在後續範圍中可見,當你重新使用 let a = 20 時,其實是建立了另一個範圍,而這個新的範圍中有個 a20,跟前一個範圍的 a 沒有關係。

read 與 mod 函式

如果你在 GHCI 中使用 :t getLine,這會告訴你 getLine 函式的型態為 getLine :: IO String,簡單來說,這表示 <- 取出的值會是 String,可是我們需要一個 Int 才能進行運算啊?你可以使用 read 函式,這可以將字串轉換為指定的型態,read input::Int 表示將 Stringinput 值轉換為 Int 值後傳回。

mod 函式是餘除函式,它會傳回兩數相除後的餘數,基本上你可以使用 mod 10 2 呼叫函式,表示要計算出 10 除以 2 的餘數,例如:

mod 函式

基本上,你也可以看到,Haskell 中呼叫函式並給定引數時,並不使用括號。不過,我們希望這類有兩個運算元的函式呼叫可以比較像數學式一些,這時你可以使用如上使用 ` 來括住函式。

if … then … else …

在其他程式語言中,會有 if..else 流程控制語法,在 Haskell 中是 if ... then ... else ...,它是個運算式,也就是說它會有傳回值:

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)的函式,像是 putStrgetLine 或這邊自訂的 prompt

先將目光放在像 modread 或這邊自訂的 descOddEven,因為在這類函式中,你無法改變參數值,只能引用它的值,因此,只要指定的引數相同,無論呼叫多少次函式,結果都會是相同的,也就是這類函式沒有副作用,相同函式在相同引數下,可預期會有相同的結果,這樣的特性稱為引用透明性(Referential Transparency),這樣的透明性能使得你容易驗證函式的正確性。

透過適當的函式封裝了一些流程,現在 main 中的主流程就清楚多了:顯示提示文字、取得輸入、計算出奇偶描述字串、顯示格式化的結果。

當然,這篇的標題寫明了是「初探」,這表示在 Haskell 中,函式還有更多值得探討的地方 …