Skip to main content

Functions in Haskell

提示
  • 本 Handout 备份于此处。(不保证与源文件同步)
  • Read also Chapter 4 of the text book "Programming in Haskell.

概述

在本课中,我们将学习如何在 Haskell 中定义函数的各种方法。我们将学习以下几种方式:

  1. Composition of existing functions
  2. Conditionals (if _ then _ else _ )
  3. Guarded equations
  4. Pattern matching
  5. 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

模式匹配根据输入的构建方式来分析输入。输入与一连串的模式匹配;第一个匹配的模式决定了函数的输出。

概述

一个布尔值只有两种可能,TrueFalse。 因此,只要有这两种情况的模式就足够了。

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

如果一个函数将两个布尔运算作为输入,则有 22=42^2=4 种模式。

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] 中的每个列表要么是

  1. 空列表;或
  2. of the form x:xs for x :: a and xs :: [a].
isEmpty' :: [a] -> Bool
isEmpty' [] = True
isEmpty' (x:xs) = False

我们实际上没有在第二个模式的输出中使用 xxs,所以我们可以将该函数更简单地写为

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

  1. 任何二进制函数都可以通过用反斜线括起来变成一个运算符。例如:div 7 2 可以写成 7 `div` 2
  2. 反过来说,任何运算符都可以用小括号括起来作为前缀,例如,(:) 1 [2,3]

每个运算符 的输入类型为 ab,输出类型为 c,会产生三个 sections

  1. (⊗) :: a -> b -> c. Here, (⊗) = \x y -> x ⊗ y.
  2. (x ⊗) :: b -> c, where x :: a. Here, (x ⊗) = \y -> x ⊗ y.
  3. (⊗ y) :: a -> c, where y :: b. Here, (⊗ y) = \x -> x ⊗ y.

Sections 可以用来简明地定义函数。

square :: Int -> Int
square = (^2)

reci :: Fractional a => a -> a
reci = (1 /)

备注:

  1. 操作符 本身不是一个有效的 Haskell 表达式:它需要作为一个部分使用,例如,(⊗)
  2. 在使用高阶函数编程时,Sections 是非常有用的(参见后面的课程)。

练习

(改编自《Haskell 编程》一书并加以扩充)

  1. 定义一个函数 third :: [a] -> a 的三个变体,该函数返回任何包含至少这么多元素的列表中的第三个元素,使用
    1. head and tail
    2. list indexing !!
    3. pattern matching
  2. 定义一个函数 safetail :: [a] -> [a],除了将 [] 映射到 [](而不是抛出一个错误),它的行为与 tail 相似。使用 tailisEmpty :: [a] -> Bool。 定义 safetail,使用
    1. a conditional expression
    2. guarded equations
    3. pattern matching

测试时间

通过参加这个测验来测试你的理解。

See also

  1. Chapter 3, "Syntax in Functions" of "Learn You a Haskell"
  2. Haskell Wiki on Sections

总结

  1. 我们已经看到了几种定义函数的方法:composition, conditionals, guard equations, pattern matching, lambda expressions.
  2. 当模式不是详尽的时候,只要没有模式匹配,函数就会引发一个异常。为了避免这种情况,我们可以在结尾处使用一个全面的 otherwise 模式。
  3. 任何模式匹配都可以用 case 表达式来表达。
  4. 匿名函数可以用 lambda 表达式简洁地编写。