Back
Featured image of post Haskell 学习笔记08 - 函数式风格

Haskell 学习笔记08 - 函数式风格

导航页

本章将介绍 Haksell 的函数式惯用写法。

实参和形参

先前我们已经讨论过柯里化。表面上,Haskell 允许出现多个形参;但实际上,所有函数都取得一个实参,并返回一个结果。可以通过各种语法,表示一个需要实参的表达式。

设置形参

Haskell 的函数声明就不再赘述了。但有一点值得注意,当形参被应用实参时,我们称其为已绑定(bound)或统一(unified)。

没有形参的示例:

myNum :: Integer 
myNum = 1

myVal = myNum

此时 myVal :: Integer.

两者是等价的。因为 myVal 没有任何形参,所以不能再应用了。

引入参数 f:

myNum :: Integer
myNum = 1

myVal f = myNum

尽管我们未使用 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
addOne x = x + 1

addOne 1  -- x is bound to 1
addOne 10 -- x is bound to 10

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

无论输入为何,输出将保持不变。因为函数的 xletx 所遮蔽。x 将保持 10

这意味着,Haskell 是词法范围的(lexically scoped),即会通过位置、词法上下文解析一个命名实体。

匿名函数

通过反斜线开头的 lambda 语法,可以定义匿名函数(anonymous function)。例如,有一个函数:

triple :: Integer -> Integer
triple x = x * 3

它的匿名函数版本:

(\x = x * 3) :: Integer -> Integer

如果要显式指明类型就需要括号。如果想的话,你可以让匿名函数不再匿名:

triple :: Integer -> Integer
triple = \x -> x * 3

直接应用参数,也需要括号:

(\x -> x * 3) 5

匿名函数在现在看来可能没什么用,但这是使用高阶函数的必须品。

模式匹配

模式匹配(pattern matching)是 Haskell 的一项重要特性。

模式匹配如其名,将值与特定模式对比并绑定。通过解构和模式匹配语法,可以更轻松的表达逻辑。

模式基于值或数据构造器匹配,而不是类型。当一个模式被匹配,相关变量将会绑定在该模式中。

例如对数字的模式匹配:

isItTwo :: Integer -> Bool
isItTwo 2 = True
isItTwo _ = False

可以这样来在 GHCi 中输入块表达式:

:{
isItTwo :: Integer -> Bool
isItTwo 2 = True
isItTwo _ = False
:}

处理所有情况

顺序很重要。如果把语句交换位置:

isItTwo :: Integer -> Bool
isItTwo _ = False
isItTwo 2 = True
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 类型,模式匹配可以打成两个目的:

  1. User 是两个构造器 UnregisteredUserRegisteredUser的并集。可以通过模式匹配为两者委派不同的方法。
  2. 对于 RegisteredUser,它是两个 newtype 的交集类型。模式匹配可以解构 RegisteredUser 并获得 UsernameAccountNumber 的内容。

编写一个打印该类型的函数:

printUser :: User -> IO ()
printUser UnregisteredUser = putStrLn "UnregisteredUser"
printUser
  ( RegisteredUser
      (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
isSouthAfrica SouthAfrica = True
isSouthAfrica Galapagos = False
isSouthAfrica Antarctica = False
isSouthAfrica Australia = False
isSouthAfrica SouthAmerica = False

上面的实现显然可行,但不够简洁:

isSouthAfrica :: WherePenguinsLive -> Bool
isSouthAfrica SouthAfrica = True
isSouthAfrica _ = False

现在看起来好多了。以及一个解构示例:

gimmeWhereTheyLive :: Penguin -> WherePenguinsLive
gimmeWhereTheyLive (Peng whereitlives) = whereitlives

解构 + _

galapagosPenguin :: Penguin -> Bool 
galapagosPenguin (Peng Galapagos) = True 
galapagosPenguin _ = False

antarcticPenguin :: Penguin -> Bool 
antarcticPenguin (Peng Antarctica) = True 
antarcticPenguin _ = False

匹配元组

交换元组位置的函数:

f :: (a, b) -> (c, d) -> ((b, d), (a, c)) 
f x y = ((snd x, snd y), (fst x, fst y))

看起来略微复杂。用模式匹配可以更清晰得表达语义:

f :: (a, b) -> (c, d) -> ((b, d), (a, c)) 
f (a, b) (c, d) = ((b, d), (a, c))

正如它的类型。

同样的,可以在函数名中使用 _ 表示忽略或通配:

fst3 :: (a, b, c) -> a
fst3 (x, _, _) = 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 
returnLast _ _ _ d = d

以上函数实质上会被柯里化为:

returnLast :: a -> (b -> (c -> (d -> d))) 
returnLast _ _ _ d = d

->右结合的,正如上面看到的,所以下面这样写不行:

returnBroke :: (((a -> b) -> c) -> d) -> d 
returnBroke _ _ _ d = d

以上类型签名实质上要求 1 个实参,它的类型为 ((a -> b) -> c) -> d。而实际定义需要 4 个实参。

因此,若想要输入一个函数,就可以像刚才那样写:

returnAfterApply :: (a -> b) -> a -> c -> b
returnAfterApply f a c = f a

因为在左侧加括号,所以这里指的是一个单独的函数,拥有自己的形参和结果。

Guard

先前我们介绍了 if-then-else 表达式,本节将介绍 guard,以允许更多条件匹配。

我们可以用 Guard 重新编写以下 if-then-else 表达式:

myAbs :: Integer -> Integer 
myAbs x = if x < 0 then (-x) else x

为:

myAbs :: Integer -> Integer 
myAbs x =
  | x < 0     = (-x)
  | otherwise = x

每一个 Guard 分支都有一个等号。用管道符号 | 开启新 Guard 分支。otherwiseTrue 的别名,在这里处理其他情况。

另一个多条分支的例子:

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)

即将输入 fg 组合为 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]
f x = take 5 . enumFrom $ x
f 3
-- [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
f z xs = foldr (+) z xs

即:

f = foldr (+)
f 0 [1..5]
-- 15

这看起来清晰得多。

module Main where

add :: Int -> Int -> Int
add x y = x + y

addPF :: Int -> Int -> Int
addPF = (+)

addOne :: Int -> Int
addOne x = x + 1

addOnePF :: Int -> Int
addOnePF = (+ 1)

main :: IO ()
main = do
  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

以上,即为本文的全部内容了。

comments powered by Disqus