跳转到内容

Haskell/函数进阶

来自维基教科书,自由的教科书

这里介绍几个让函数使用起来更方便的特性。

重新认识 let 和 where

[编辑 | 编辑源代码]

正如前面章节所述,letwhere 在局部函数定义中很有用。这里,sumStr 调用了 addStr 函数

addStr :: Float -> String -> Float
addStr x str = x + read str

sumStr :: [String] -> Float
sumStr = foldl addStr 0.0

但如果我们永远不需要在其他地方使用 addStr 呢?那么我们可以使用局部绑定来重写 sumStr。我们可以用let绑定...

sumStr =
   let addStr x str = x + read str
   in foldl addStr 0.0

... 或者用 where 语句...

sumStr = foldl addStr 0.0
   where addStr x str = x + read str

... 他们的区别似乎只是风格问题:我们更喜欢绑定出现在定义的其他部分之前还是之后?

然而,letwhere 之间还有另一个重要的区别。letlet...in结构是一个表达式,就像 if/then/else 一样。相比之下,where 语句类似于守卫,因此不是表达式。因此,let 绑定可以用于复杂的表达式中

f x =
    if x > 0
        then (let lsq = (log x) ^ 2 in tan lsq) * sin x
        else 0

外层括号内的表达式是自包含的,并计算 x 对数平方的正切值。请注意,lsq 的作用域不超过括号,因此将 then 分支更改为

        then (let lsq = (log x) ^ 2 in tan lsq) * (sin x + lsq)

如果不删除 let 周围的括号,则无法正常工作。

尽管不是完整的表达式,where 语句可以被包含在 case 表达式中

describeColour c = 
   "This colour "
   ++ case c of
          Black -> "is black"
          White -> "is white"
          RGB red green blue -> " has an average of the components of " ++ show av
             where av = (red + green + blue) `div` 3
   ++ ", yeah?"

在这个例子中,where 语句的缩进设置了变量 av 的作用域,使其仅在where语句的作用域内有效,即只有在av变量所在的RGB red green bluecase 语句中有效。如果将 where 语句放在与 case 语句相同的缩进级别,则它将对所有 case 语句有效。以下是一个使用守卫的例子

doStuff :: Int -> String
doStuff x
  | x < 3     = report "less than three"
  | otherwise = report "normal"
  where
    report y = "the input is " ++ y

注意,由于每个守卫只有一个等号,因此我们无法在 let 表达式中放置一个作用域能够覆盖所有守卫的表达式,就像 where 语句一样。因此,这种情况尤其适合使用 where 语句。

匿名函数 - lambda 表达式

[编辑 | 编辑源代码]

为什么为一个像 addStr 这样的函数创建正式的名称,而它只存在于另一个函数的定义中,永远不会被再次使用?相反,我们可以将其设为匿名函数,也称为“lambda 函数”。然后,sumStr 可以这样定义

sumStr = foldl (\ x str -> x + read str) 0.0

括号中的表达式是一个 lambda 函数。反斜杠用作希腊字母 lambda(λ)的最接近的 ASCII 等价物。这个 lambda 函数接受两个参数 xstr,并计算为“x + read str”。因此,上面介绍的 sumStr 与使用 let 绑定中 addStrsumStr 完全相同。

lambda 表达式对于编写与 map、fold 及其兄弟函数一起使用的临时函数非常方便,尤其是在该函数很简单的情况下(注意不要将复杂的表达式塞进 lambda 表达式中——这会降低可读性)。

由于 lambda 表达式中绑定了变量(绑定到参数,就像在普通函数定义中一样),因此模式匹配也可以在其中使用。一个简单的例子是使用 lambda 表达式重新定义 tail

tail' = (\ (_:xs) -> xs)

注意:由于 lambda 表达式在 Haskell 中是一个特殊字符,因此 \ 本身将被视为函数,而紧随其后的任何非空格字符将作为第一个参数的变量。最好在 lambda 和参数之间加上空格(尤其是在 lambda 接受多个参数时,这使得代码更易读)。

运算符

[编辑 | 编辑源代码]

在 Haskell 中,任何接受两个参数并且名称完全由非字母数字字符组成的函数都被认为是运算符。最常见的例子是算术运算符,如加法 (+) 和减法 (-)。与其他函数不同,运算符通常用中缀形式(写在两个参数之间)使用。所有运算符也可以用括号括起来,然后像其他函数一样用前缀形式使用

-- these are the same:
2 + 4
(+) 2 4

我们可以像其他函数一样以常规方式定义新运算符——只是不要在它们的名称中使用任何字母数字字符。例如,以下是从 Data.List 定义的集合差运算符

(\\) :: (Eq a) => [a] -> [a] -> [a]
xs \\ ys = foldl (\zs y -> delete y zs) xs ys

如上例所示,运算符也可以以中缀形式定义。以前缀形式编写的相同定义也适用

(\\) xs ys = foldl (\zs y -> delete y zs) xs ys

注意,运算符的类型声明没有中缀版本,必须用括号括起来。

是一个很巧妙的语法糖,可以用在运算符上。括号中的运算符,两侧都有其参数之一...

(2+) 4
(+4) 2

... 本身就是一个新的函数。例如,(2+) 的类型是 (Num a) => a -> a。我们可以将节传递给其他函数,例如 map (+2) [1..4] == [3..6]。再举一个例子,我们可以为我们在列表 II 中编写的 multiplyList 函数添加额外的修饰

multiplyList :: Integer -> [Integer] -> [Integer]
multiplyList m = map (m*)


如果你有一个“普通”的前缀函数,想要将其用作运算符,只需用反引号将其括起来即可

1 `elem` [1..4]

这被称为使函数中缀。通常这样做是为了提高可读性:1 `elem` [1..4]elem 1 [1..4] 阅读起来更好。你也可以定义中缀函数

elem :: (Eq a) => a -> [a] -> Bool
x `elem` xs = any (==x) xs

但再次注意,类型签名仍然保持前缀风格。

节甚至可以与中缀函数一起使用

(1 `elem`) [1..4]
(`elem` [1..4]) 1

当然,请记住,你只能使二元函数(即接受两个参数的函数)成为中缀函数。

练习
  • lambda 表达式是避免定义不必要单独函数的一种好方法。将以下 let 或 where 绑定转换为 lambda 表达式
    • map f xs where f x = x * 2 + 3
    • let f x y = read x + y in foldr f 1 xs
  • 节只是 lambda 操作的语法糖。即 (+2) 等价于 \x -> x + 2。以下节将“反糖化”成什么?它们的类型是什么?
    • (4+)
    • (1 `elem`)
    • (`notElem` "abc")
华夏公益教科书