Haskell簡介[2]--函式的宣告與各種語法

喔呼連續發兩篇文呢><
在這篇文中主要會介紹定義函式時會使用的語法
然後這應該是最後一篇單純介紹語法的Haskell簡介了><

函式定義

在Haskell中定義函式的方法很簡單,例如若要定義一個平方函式,只要square x = x^2即可。
注意Haskell的函數必須是小寫字母開頭,而大寫字母開頭的東西被留給型別的建構子。
一般而言為了避免一些不必要的問題,在寫函式時會指定他的型別,如

1
2
square :: Int -> Int
square x = x^2

注意:要在ghci打入多行程式時,需要在開頭結尾分別用:{:}包住。注意兩個都要各佔一行。

Haskell也支援這種寫法

1
2
3
4
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib x = (fib (x-1)) + (fib (x-2))

每次呼叫fib,Haskell會由上至下看看那一個符合參數的樣子(也就是模式匹配, pattern matching),並執行第一個符合的定義。但是這些定義必須要一次寫完,中間不能空行,更不能在程式碼的另外一個地方把定義補完。
又或是

1
2
showSum :: (Int, Int) -> String
showSum p@(a, b) = "The sum of " ++ (show p) ++ " is " ++ (show (a + b)) ++ "."

p@(a, b)代表p會匹配到整個二元組,而ab會分別匹配到二元組的兩項。

多引數時也是一樣。以下我們來試圖自己實做take函式,稱為take'[1]

1
2
3
4
5
-- self-implemented take
take' :: Int -> [a] -> [a]
take' _ [] = []
take' 0 _ = []
take' x y = (head y):(take' (x-1) $ tail y)

其中_的意思就是我們不在乎那個參數的值。
而在haskell中的單行註解是以--開頭,多行註解是以{--}包住。

運算子

另外,運算子也可以用同樣的方法被定義。若一個函式的名稱僅由符號組成,那它就是一個運算子。例如

1
2
(.*.) :: [a] -> [b] -> [(a, b)]
(.*.) a b = [(i, j) | i <- a, j <- b]

接著不論是運算子,或是以`function`定義的中綴函數,都可被定義是左結合或右結合,以及他的優先度(fixity)。優先度由09,越高者越先被計算。
語法是[infix|infixr|infixl] (fixity) op,例如infix 3 (.*.)或是infixl `op`
預設的運算子是infixl 9,而預設的中綴函數是infix 9

以上這種定義函數的方法其實是λ函式的語法糖。

λ函式

所謂λ函式就是inline function。
語法是square = \x -> x^2。或是加上型別是square = (\x -> x^2)::Int -> Int
那個\就是λ的意思[2]
當然也可以有多變數函數add = \x -> \y -> x + y
或是語法糖可以讓它簡化成\x y -> x + y

常用語法

let ... in

用法是let x = 3 in x * x,是(\x -> x * x) 3的語法糖。
let ... in中也可以定義函式。如let square x = x*x in square 3
或甚至可以定義多於一個函式[3],如

1
2
3
let y = 3
square x = x*x
in square y

注意那個縮排[4]
let ... in中還可以做模式匹配,但如果無法匹配時就會丟出錯誤。
let (a, b) = p in a + b就是把二元組的兩個元素相加。

守衛子句與where

除了以上的寫法,有時我們需要定義一些能橫跨不同定義的函式。這時就可以使用守衛子句的寫法。以atan2為例:

1
2
3
4
5
6
7
8
9
atan2 :: Float -> Float -> Float
atan2 y x
| x > 0 = angle
| (x < 0) && (y >= 0) = angle + pi
| (x < 0) && (y < 0) = angle - pi
| y > 0 = pi / 2
| y < 0 = - pi / 2
| otherwise = undefined
where angle = atan (y / x)

在這裡otherwise是為了讓守衛子句比較清楚而在Prelude定義為Trueundefined是一個當被執行就會丟出錯誤的函式。

if ... then ... else

正常的三元運算子。例如even x = if (x `mod` 2) == 1 then False else True

case ... of

當想要在函式的其他地方進行模式匹配使得不同模式會有不同結果時,可以使用case ... of。 例如

1
2
3
4
5
6
7
8
say :: Int -> String
say i = "You are the " ++ (show i) ++ (
case i of
1 -> "st"
2 -> "nd"
3 -> "rd"
x -> "th"
) ++ "."

舉例

讀者可以比較一下以上幾種不同的語法實際寫起來有什麼不同
(在這裡偷一下learn you a haskell的例子):

1
2
3
4
describeList :: [a] -> String
describeList [] = "The list is empty."
describeList [x] = "The list is a singleton list."
describeList xs = "The list is a longer list."
1
2
3
4
5
6
7
8
describeList :: [a] -> String
describeList xs = "The list is " ++
if null xs
then "empty."
else if (length xs) == 1
then "a singleton list."
else
"a longer list."
1
2
3
4
5
describeList :: [a] -> String
describeList xs = "The list is " ++ what xs
where what [] = "empty."
what [x] = "a singleton list."
what xs = "a longer list."
1
2
3
4
5
describeList :: [a] -> String
describeList xs = "The list is " ++ case xs of
[] -> "empty."
[x] -> "a singleton list."
xs -> "a longer list."

問題:你能自己寫出上一篇提到的Prelude裡面包含的各個函式嗎?

前篇解答

問題:請寫出所有三邊長小於100的畢式直角三角形。

1
[(i, j, k)| i <- [1..100], j <- [1..100], k <- [1..100], i^2+j^2 == k^2]

問題:什麼是flip const const
const a = \b -> a => const a b = a =>
(flip const) b a = a => (flip const) const a = a =>
flip const const = \a -> a = id

問題:什麼又是curry fst
fst = \(a, b) -> a => curry fst = \a b -> a = const

問題:什麼又是((not.).(null.).filter.(==))
就是elem喔~

後語

這次就先到這裡了,下次開始會進入有趣的內容唷><
敬請期待~
喔然後有錯的話請來敲我>< 八成錯誤百出QQ


  1. haskell的取名是可以包含'的喔

  2. 聽說長得很像

  3. 對haskell而言,常數其實就是沒有參數的函式

  4. Haskell的縮排規則很簡單,只要將同一level的東西對齊即可。例如這裡的定義們,或是之後守衛子句或case ... of的cases也都需要對齊。
    另外,也可以使用大括號和分號來忽略縮排,例如let { y = 3; square x = x * x } in square y。一個分號代表一個換行。