Functions in Haskell
- 本 Handout 备份于此处。(不保证与源文件同步)
- Read also Chapter 4 of the text book "Programming in Haskell.
概述
在本课中,我们将学习如何在 Haskell 中定义函数的各种方法。我们将学习以下几种方式:
- Composition of existing functions
- Conditionals (
if _ then _ else _
) - Guarded equations
- Pattern matching
- Lambda expressions
在最后,我们还将看看 operators(infix 函数符号,如++
),以及如何将它们变成函数。
组成函数 Composing functions
A video for this section, including explanation of the exercise, is here.
我们可以对现有的函数进行组合以获得新的函数。 For instance:
removeLast :: [a] -> [a]
removeLast xs = reverse (tail (reverse xs))
removeElem :: Int -> [a] -> [a]
removeElem n xs = removeLast (take n xs) ++ drop n xs
练习: Using the functions above, write a function that removes both the first and the last element of a list.
答案
removeFirstAndLast :: [a] -> [a]
removeFirstAndLast xs = removeLast (removeElem 1 xs)
条件式 Conditionals
Haskell provides if _ then _ else _
. It is typed Bool -> a -> a -> a
, 多态地.
abs' :: Integer -> Integer
abs' n = if n >= 0 then n else -n
Note: The else
branch is 强制性.
We can nest if _ then _ else _
:
howMuchDoYouLikeHaskell :: Int -> String
howMuchDoYouLikeHaskell x = if x < 3 then "I dislike it!" else
if x < 7 then "It's ok!" else
"It's fun!"
然而,这很难读懂;guarded equations(见下文)可以更令人愉快地阅读。我们将避免使用条件式。
Exercise: Read the discussion about if _ then _ else _
on the Haskell wiki.
受保护的方程式 Guarded equations
A video for this section, including explanation for the exercise, is here.
Guarded equations are an alternative to if _ then _ else _
expressions. They are often more readable:
abs :: Int -> Int
abs n | n >= 0 = n
| otherwise = -n
Here, n >= 0
and otherwise
are called guards; 它们是布尔运算。
The function returns the value after the first guard that evaluates to True
.
Guarded equations are more convenient to use than if _ then _ else _
:
howMuchDoYouLikeHaskell2 :: Int -> String
howMuchDoYouLikeHaskell2 x | x < 3 = "I dislike it!"
| x < 7 = "It's ok!"
| otherwise = "It's fun!"
练习: 使用 guarded equations,写一个 Int -> Int -> Bool
类型的函数,如果第一个参数大于第二个参数且小于第二个参数的两倍,则返回 True
。
答案
answer :: Int -> Int -> Bool
answer x y | x > y && x < 2 * y = True
| otherwise = False
模式匹配 Pattern matching
模式匹配根据输入的构建方式来分析输入。输入与一连串的模式匹配;第一个匹配的模式决定了函数的输出。
概述
一个布尔值只有两种可能,True
或 False
。
因此,只要有这两种情况的模式就足够了。
notB :: Bool -> Bool
notB False = True
notB True = False
只有一种方法能让人成双成对:
swap :: (a, b) -> (b, a)
swap (x,y) = (y,x)
有两种方法来制作清单:
isEmpty :: [a] -> Bool
isEmpty [] = True
isEmpty (x:xs) = False
关于布尔运算 On Booleans
最简单的模式之一是为布尔语匹配。
如果输入的只是一个布尔值,就只有两种模式:
notB' :: Bool -> Bool
notB' False = True
notB' True = False
如果一个函数将两个布尔运算作为输入,则有 种模式。
andB :: Bool -> Bool -> Bool
andB True True = True
andB True False = False
andB False True = False
andB False False = False
后面三个模式可以组合。在这里,通配符模式 _
匹配任何东西,并将其丢弃。
andB' :: Bool -> Bool -> Bool
andB' True True = True
andB' _ _ = False
这两个版本之间有一个区别:在后者中,如果第一个参数是 False
,那么第二个参数就不需要被评估。False
会立即返回。
在下一个例子中,模式 b
匹配任何东西。然而,与 _
相反,我们可以在 =
的右边使用 b
。
andB'' :: Bool -> Bool -> Bool
andB'' True b = b
andB'' False _ = False
练习: Write a function orB :: Bool -> Bool -> Bool
that returns True
if at least one argument is True
.
答案
orB :: Bool -> Bool -> Bool
orB True _ _ = True
orB _ True _ = True
orB _ _ True = True
非穷举模式 Non-exhaustive patterns
请考虑以下例子:
isTrue :: Bool -> Bool
isTrue True = True
问题: isTrue False
将被评估为什么?
答案: 这是一个非详尽的模式,isTrue False
将引发一个异常:
*Main> isTrue False
*** Exception: defining-functions.hs:36:1-18: Non-exhaustive patterns in function isTrue
我们可以选择抛出一个自定义的异常来代替:
isTrue' :: Bool -> Bool
isTrue' True = True
isTrue' False = error "not True"
关于图元 On tuples
如果我们定义的函数期望输入的是一个元组,我们就可以对各个组成部分进行匹配。
fst :: (a,b) -> a
fst (x,y) = x
实际上我们在函数的输出中没有使用 y
,所以我们可以用 fst (x,_) = x
来代替。类似地:
snd :: (a,b) -> b
snd (_,y) = y
这可以推广到三个或更多成分的图元:
third :: (a, b, c) -> c
third (_, _, z) = z
我们可以同时匹配几个图元:
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
练习: 写一个函数 swap :: (a, b) -> (b, a)
,将一对元素互换。
答案
swap :: (a, b) -> (b, a)
swap (1, 2) = (2, 1)
关于名单 On lists
See also this video.
所有的列表都是通过在现有的列表中连续添加 (:)
元素来建立的,从空列表 []
开始。这意味着,列表 [1, 2, 3]
已经得到 1:[2,3]
,等等。- [1, 2, 3]
是 1:(2:(3:[])
的简称。换句话说,[a]
中的每个列表要么是
- 空列表;或
- of the form
x:xs
forx :: a
andxs :: [a]
.
isEmpty' :: [a] -> Bool
isEmpty' [] = True
isEmpty' (x:xs) = False
我们实际上没有在第二个模式的输出中使用 x
或 xs
,所以我们可以将该函数更简单地写为
isEmpty'' :: [a] -> Bool
isEmpty'' [] = True
isEmpty'' (_:_) = False
请注意,第二种模式中 _:_
周围的括号是必要的!
我们可以编写更复杂的列表模式。要返回一个列表的第二个元素:
sndElem :: [a] -> a
sndElem (_:x:_) = x
案例表达 Case expressions
前面提到的模式是特殊的形式,用于布尔运算,和列表。这种模式匹配的一般形式是通过 case
表达式。
isEmpty2 :: [a] -> Bool
isEmpty2 x = case x of [] -> True
(_:_) -> False
在这里,重要的是所有的模式要完全对齐,即 []
和 (_:_)
必须在完全相同的列开始。
Lambda 表达式 Lambda expressions
Lambda 表达式是无名的函数。它们在高阶函数中特别有用,这将在后面的课程中讨论。
A video accompanying this section is here.
兰姆达表达式的形式是 \<input variables> -> <output>
。例如,我们可以定义一个函数,将其双倍数返回为 \x -> 2 * x
。这里,输入变量由反斜杠 \
表示。在箭头 ->
之后,指定了函数的输出。(\
代表希腊字母 λ(lambda),见Wikipedia)因此,以下定义是等同的:
double :: Int -> Int
double x = 2 * x
double' :: Int -> Int
double' = \x -> 2 * x
Lambda 表达式可以有多个输入变量。
mult :: Int -> Int -> Int
mult x y = x * y
mult' :: Int -> Int -> Int
mult' = \x y -> x * y
在这里,第二个变体是一个简短的形式
mult'' :: Int -> (Int -> Int)
mult'' = \x -> (\y -> x * y)
就像模式可以忽略(部分)输入一样,lambda 表达式可以忽略其输入
alwaysZero :: Bool -> Int
alwaysZero = \_ -> 0
lambda 表达式的一个重要应用是高阶函数,其中函数是其他函数的参数。请考虑:
apply :: (a -> b) -> a -> b
apply f x = f x
*Main> apply (\_ -> 5) 'r'
5
*Main> apply (\ x -> if x < 0 then "Less than zero!" else "Greater or equal than zero!") (-3)
"Less than zero!"
Operators and sections
There is also a video on operators and sections.
当一个函数有两个参数时,例如 (:)
,我们可以把它写成 infix,在它的两个参数之间。一个使用 infix(因此必须是二进制)的函数被称为 operator。
- 任何二进制函数都可以通过用反斜线括起来变成一个运算符。例如:
div 7 2
可以写成7 `div` 2
。 - 反过来说,任何运算符都可以用小括号括起来作为前缀,例如,
(:) 1 [2,3]
。
每个运算符 ⊗
的输入类型为 a
和 b
,输出类型为 c
,会产生三个 sections:
(⊗) :: a -> b -> c
. Here,(⊗) = \x y -> x ⊗ y
.(x ⊗) :: b -> c
, wherex :: a
. Here,(x ⊗) = \y -> x ⊗ y
.(⊗ y) :: a -> c
, wherey :: b
. Here,(⊗ y) = \x -> x ⊗ y
.
Sections 可以用来简明地定义函数。
square :: Int -> Int
square = (^2)
reci :: Fractional a => a -> a
reci = (1 /)
备注:
- 操作符
⊗
本身不是一个有效的 Haskell 表达式:它需要作为一个部分使用,例如,(⊗)
。 - 在使用高阶函数编程时,Sections 是非常有用的(参见后面的课程)。
练习
(改编自《Haskell 编程》一书并加以扩充)
- 定义一个函数
third :: [a] -> a
的三个变体,该函数返回任何包含至少这么多元素的列表中的第三个元素,使用head
andtail
- list indexing
!!
- pattern matching
- 定义一个函数
safetail :: [a] -> [a]
,除了将[]
映射到[]
(而不是抛出一个错误),它的行为与 tail 相似。使用tail
和isEmpty :: [a] -> Bool
。 定义safetail
,使用- a conditional expression
- guarded equations
- pattern matching
测试时间
通过参加这个测验来测试你的理解。
See also
总结
- 我们已经看到了几种定义函数的方法:composition, conditionals, guard equations, pattern matching, lambda expressions.
- 当模式不是详尽的时候,只要没有模式匹配,函数就会引发一个异常。为了避免这种情况,我们可以在结尾处使用一个全面的
otherwise
模式。 - 任何模式匹配都可以用
case
表达式来表达。 - 匿名函数可以用 lambda 表达式简洁地编写。