來寫個簡單的程式,可以接受使用者輸入整數,判斷其為奇數或偶數,下面這個範例是以〈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 中,函式還有更多值得探討的地方 …