本章将介绍 Haksell 的函数式惯用写法。
实参和形参
先前我们已经讨论过柯里化。表面上,Haskell 允许出现多个形参;但实际上,所有函数都取得一个实参,并返回一个结果。可以通过各种语法,表示一个需要实参的表达式。
设置形参
Haskell 的函数声明就不再赘述了。但有一点值得注意,当形参被应用实参时,我们称其为已绑定(bound)或统一(unified)。
没有形参的示例:
myNum :: Integer
= 1
myNum
= myNum myVal
此时 myVal :: Integer
.
两者是等价的。因为 myVal
没有任何形参,所以不能再应用了。
引入参数 f
:
myNum :: Integer
= 1
myNum
= myNum myVal f
尽管我们未使用
f
,但确实有一个形参待应用:myVal :: t -> Integer
。同时显然的,f
是完全多态的类型 t
,也是因为没有使用。
如果使用的话,那么它的类型会更加具体:
Prelude> myNum = 1 :: Integer
Prelude> myVal f = f + myNum
Prelude> :t myVal
myVal :: Integer -> Integer
值得注意的是,在定义上,Haskell 没有无参函数,仅仅有值。而多个参数的函数也会被柯里化,变为嵌套的单参函数。这在先前已经提到过。
变量绑定
函数定义中的绑定:
addOne :: Integer -> Integer
= x + 1
addOne x
1 -- x is bound to 1
addOne 10 -- x is bound to 10 addOne
where
子句:
bindExp :: Integer -> String
=
bindExp x let y = 5
in "the integer was: " ++ show x
++ "and y was: " ++ show y
此处 y
绑定在 let
表达式的作用域中。因此这样是不行的:
bindExp :: Integer -> String
=
bindExp x let z = y + x
in let y = 5
in "the integer was: "
++ show x
++ " and y was: "
++ show y
++ " and z was: "
++ show z
let z = y + x
中无法获取到
y
,因为它在更深一级。在父作用域中无法访问子作用域的绑定。
以及变量遮蔽(shadow)的例子:
bindExp :: Integer -> String
=
bindExp x let x = 10; y = 5
in "the integer was: " ++ show x
++ " and y was: "
++ show y
无论输入为何,输出将保持不变。因为函数的 x
被
let
的 x
所遮蔽。x
将保持
10
。
这意味着,Haskell 是词法范围的(lexically scoped),即会通过位置、词法上下文解析一个命名实体。
匿名函数
通过反斜线开头的 lambda 语法,可以定义匿名函数(anonymous function)。例如,有一个函数:
triple :: Integer -> Integer
= x * 3 triple x
它的匿名函数版本:
= x * 3) :: Integer -> Integer (\x
如果要显式指明类型就需要括号。如果想的话,你可以让匿名函数不再匿名:
triple :: Integer -> Integer
= \x -> x * 3 triple
直接应用参数,也需要括号:
-> x * 3) 5 (\x
匿名函数在现在看来可能没什么用,但这是使用高阶函数的必须品。
模式匹配
模式匹配(pattern matching)是 Haskell 的一项重要特性。
模式匹配如其名,将值与特定模式对比并绑定。通过解构和模式匹配语法,可以更轻松的表达逻辑。
模式基于值或数据构造器匹配,而不是类型。当一个模式被匹配,相关变量将会绑定在该模式中。
例如对数字的模式匹配:
isItTwo :: Integer -> Bool
2 = True
isItTwo = False isItTwo _
可以这样来在 GHCi 中输入块表达式:
:{ isItTwo :: Integer -> Bool isItTwo 2 = True isItTwo _ = False :}
处理所有情况
顺序很重要。如果把语句交换位置:
isItTwo :: Integer -> Bool
= False
isItTwo _ 2 = True isItTwo
interactive>:9:33: Warning:
Pattern match(es) are overlapped
In an equation for ‘isItTwo’:
isItTwo 2 = ...
Prelude> isItTwo 2
False
Prelude> isItTwo 3
False
无论如何都将会优先匹配 _
。
模式匹配子句时,应当以从特例到最普遍的顺序排列。因此 _
往往在最后。
当然,之前已经说过很多次了。应当避免偏函数,模式匹配的分支应当是完备的。
匹配数据构造器
接下来我们将使用 newtype
作为 data
声明的特例。newtype
略有不同,它只仅允许一个构造器、一个成员。
newtype Username =
Username String
newtype AccountNumber =
AccountNumber Integer
data User =
UnregisteredUser
| RegisteredUser Username AccountNumber
对于 User
类型,模式匹配可以打成两个目的:
User
是两个构造器UnregisteredUser
和RegisteredUser
的并集。可以通过模式匹配为两者委派不同的方法。- 对于
RegisteredUser
,它是两个newtype
的交集类型。模式匹配可以解构RegisteredUser
并获得Username
和AccountNumber
的内容。
编写一个打印该类型的函数:
printUser :: User -> IO ()
UnregisteredUser = putStrLn "UnregisteredUser"
printUser
printUserRegisteredUser
( Username name)
(AccountNumber acctNum)
(= putStrLn $ name ++ " " ++ show acctNum )
不妨查看相关类型:
Prelude> :t RegisteredUser
RegisteredUser :: Username -> AccountNumber -> User
Prelude> :t Username
Username :: String -> Username
Prelude> :t AccountNumber
AccountNumber :: Integer -> AccountNumber
RegisteredUser
的类型为
Username -> AccountNumber -> User
,这就是之前所说的数据构造器。
接下来可以试试使用 printUser
:
Prelude> printUser UnregisteredUser
UnregisteredUser
Prelude> printUser $ RegisteredUser (Username "David") (AccountNumber 114514)
David 114514
通过使用模式匹配,我们能够解构 User
类型的
RegisteredUser
值,并在不同类型的构造函数上改变行为。
看看另一个例子:
data WherePenguinsLive
= Galapagos
| Antarctica
| Australia
| SouthAfrica
| SouthAmerica
deriving (Eq, Show)
data Penguin
= Peng WherePenguinsLive
deriving (Eq, Show)
我们可以编写一个 isSouthAfrica
函数,来判断该值是否为南非:
isSouthAfrica :: WherePenguinsLive -> Bool
SouthAfrica = True
isSouthAfrica Galapagos = False
isSouthAfrica Antarctica = False
isSouthAfrica Australia = False
isSouthAfrica SouthAmerica = False isSouthAfrica
上面的实现显然可行,但不够简洁:
isSouthAfrica :: WherePenguinsLive -> Bool
SouthAfrica = True
isSouthAfrica = False isSouthAfrica _
现在看起来好多了。以及一个解构示例:
gimmeWhereTheyLive :: Penguin -> WherePenguinsLive
Peng whereitlives) = whereitlives gimmeWhereTheyLive (
解构 + _
:
galapagosPenguin :: Penguin -> Bool
Peng Galapagos) = True
galapagosPenguin (= False
galapagosPenguin _
antarcticPenguin :: Penguin -> Bool
Peng Antarctica) = True
antarcticPenguin (= False antarcticPenguin _
匹配元组
交换元组位置的函数:
f :: (a, b) -> (c, d) -> ((b, d), (a, c))
= ((snd x, snd y), (fst x, fst y)) f x y
看起来略微复杂。用模式匹配可以更清晰得表达语义:
f :: (a, b) -> (c, d) -> ((b, d), (a, c))
= ((b, d), (a, c)) f (a, b) (c, d)
正如它的类型。
同样的,可以在函数名中使用 _
表示忽略或通配:
fst3 :: (a, b, c) -> a
= x fst3 (x, _, _)
case
表达式
类似 if-then-else
表达式,when
也可以根据值的不同返回不同的结果。只要数据类型有可见的数据构造器,那么就可以搭配
when
。
无论何时,我们在对某个并集类型模式匹配时,都需要处理每个构造器,或者提供默认分支匹配全部。必须匹配所有情况,否则就会得到一个偏函数,导致运行时异常。然而,手动处理每个输入的情况很少见。
接下来我们介绍 case
语法。这是一个
if-then-else
语句:
if x + 1 == 1 then "AWESOME" else "wut"
用 case
重写:
=
funcZ x case x + 1 == 1 of
True -> "AWESOME"
False -> "wut"
语法不同,但结果都一样。以及更多示例:
-- 判断是否为回文 *pal*indrome
=
pal x case xs == reverse xs of
True -> "Yes"
False -> "no"
-- 或者
=
pal' x case y of
True -> "Yes"
False -> "no"
where y = xs == reverse xs
高阶函数
高阶函数(higher-order functions, HOF)是接受函数作为实参的函数。函数是值,因此也可以作为参数传递。
例如,高阶函数 flip
,它交换两个实参的位置:
Prelude> :t flip
flip :: (a -> b -> c) -> b -> a -> c
-- 这里的 (-) 即为 (a -> b -> c)
Prelude> (-) 10 1
9
Prelude> let fSub = flip (-)
Prelude> fSub 10 1
-9
Prelude> fSub (-10) 5
15
Prelude> fSub 5 10
5
flip
的实现很简单:
flip :: (a -> b -> c) -> b -> a-> c
flip f x y = f y x
当参数为函数时,需要用括号包裹签名。此处 f
的签名即为
a -> b -> c
。
为了更好理解高阶函数的语义,应当记住在类型签名中括号是如何结合的。
returnLast :: a -> b -> c -> d -> d
= d returnLast _ _ _ d
以上函数实质上会被柯里化为:
returnLast :: a -> (b -> (c -> (d -> d)))
= d returnLast _ _ _ d
->
是右结合的,正如上面看到的,所以下面这样写不行:
returnBroke :: (((a -> b) -> c) -> d) -> d
= d returnBroke _ _ _ d
以上类型签名实质上要求 1 个实参,它的类型为
((a -> b) -> c) -> d
。而实际定义需要 4
个实参。
因此,若想要输入一个函数,就可以像刚才那样写:
returnAfterApply :: (a -> b) -> a -> c -> b
= f a returnAfterApply f a c
因为在左侧加括号,所以这里指的是一个单独的函数,拥有自己的形参和结果。
Guard
先前我们介绍了 if-then-else
表达式,本节将介绍
guard,以允许更多条件匹配。
我们可以用 Guard 重新编写以下 if-then-else
表达式:
myAbs :: Integer -> Integer
= if x < 0 then (-x) else x myAbs x
为:
myAbs :: Integer -> Integer
=
myAbs x | x < 0 = (-x)
| otherwise = x
每一个 Guard 分支都有一个等号。用管道符号 |
开启新 Guard
分支。otherwise
是 True
的别名,在这里处理其他情况。
另一个多条分支的例子:
bloodNa :: Integer -> String
bloodNa x| x < 135 = "toolow"
| x > 145 = "toohigh"
| otherwise = "just right"
Guard 也可以匹配多个值,只要每条分支都能被求值为
Bool
:
-- 判断输入的三个数
-- 是否是合理的三角形边长
isRight :: (Num a, Eq a)
=> a -> a -> a -> Bool
isRight a b c| a ^ 2 + b ^ 2 == c ^ 2 = True
| otherwise = False
当然,也可以在 Guard 中使用 where
子句:
avgGrade :: (Fractional a, Ord a)
=> a -> Char
avgGrade x| y >= 0.9 = 'A'
| y >= 0.8 = 'B'
| y >= 0.7 = 'C'
| y >= 0.59 = 'D'
| y < 0.59 = 'F'
where y = x / 100
这里我们不必使用 otherwise
,因为分支条件已经完备。
函数组合
函数组合(function composition)是一类组合函数的高阶函数。可以使函数调用更加简洁。不妨查看函数定义:
(.) :: (b -> c) -> (a -> b) -> a -> c
.) f g = \x -> f (g x) (
即将输入 f
和 g
组合为
f (g x)
。可以这样使用:
negate . sum $ [1, 2, 3, 4, 5]
-- -15
-- 求值过程:
-- 当然, 下面这行单独放出来也是可以的
negate (sum [1, 2, 3, 4, 5])
negate 15
-15
需要使用 $
来改变优先级。negate . sum [1, 2, 3, 4, 5]
会不过编译,而
(negate . sum) [1, 2, 3, 4, 5]
显得太繁琐。所以
$
是最佳选择。
一些示例:
take 5 . reverse $ [1..10]
-- [10,9,8,7,6]
take 5 . enumFrom $ 3
-- [3,4,5,6,7]
= take 5 . enumFrom $ x
f x 3
f -- [3,4,5,6,7]
在组合两个以上函数时,可以减少嵌套:
take 5 . filter odd . enumFrom $ 3
-- [3,5,7,9,11]
-- 作为对比:
take 5 (filter odd (enumFrom 3))
Pointfree 风格
Pointfree
是一种不指定参数的组合函数风格。「Pointfree」中的「Point」指的是参数,而不是函数组合运算符
(.)
。
In some sense, we add “points” (the operator) for dropping points.
可以把下面的函数重写为 pointfree 风格的:
f :: Int -> [Int] -> Int
= foldr (+) z xs f z xs
即:
= foldr (+)
f 0 [1..5]
f -- 15
这看起来清晰得多。
module Main where
add :: Int -> Int -> Int
= x + y
add x y
addPF :: Int -> Int -> Int
= (+)
addPF
addOne :: Int -> Int
= x + 1
addOne x
addOnePF :: Int -> Int
= (+ 1)
addOnePF
main :: IO ()
= do
main print (0 :: Int)
print $ add 1 0
print $ addOne 0
print $ addOnePF 0
print $ addOne . addOne $ 0
print $ addOnePF . addOne $ 0
print $ addOne . addOnePF $ 0
print $ addOne . addOne . addOne . negate . addOne $ 0
以上,即为本文的全部内容了。