理解高阶函数
Haskell 作为一种函数式语言,支持高阶函数 (HOF)。在数学中,HOF 被称为functionals,但您不需要任何数学来理解它们。在通常的命令式编程中,比如在 Java 中,函数可以接受值,比如整数和字符串,对它们做一些事情,然后返回一个其他类型的值。
但是,如果函数本身与值没有什么不同,并且您可以接受一个函数作为参数或从另一个函数返回它呢? f a b c = a + b - c 是一个无聊的函数,它将 a 和 b 相加,然后减去 c。但是这个函数可能会更有趣,如果我们可以概括它,如果我们有时想将a 和b 相加,但有时会相乘怎么办?或者除以c而不是减去?
请记住,(+) 只是一个返回一个数字的 2 个数字的函数,它没有什么特别之处,因此任何返回一个数字的 2 个数字的函数都可以代替它。写g a b c = a * b - c、h a b c = a + b / c 等等对我们来说并不合适,我们需要一个通用的解决方案,毕竟我们是程序员!在 Haskell 中它是如何完成的:
let f g h a b c = a `g` b `h` c in f (*) (/) 2 3 4 -- returns 1.5
你也可以返回函数。下面我们创建一个函数,它接受一个函数和一个参数并返回另一个函数,该函数接受一个参数并返回一个结果。
let g f n = (\m -> m `f` n); f = g (+) 2 in f 10 -- returns 12
(\m -> m `f` n) 构造是 1 个参数 m 的 anonymous function,它将 f 应用于 m 和 n。基本上,当我们调用g (+) 2 时,我们创建了一个有一个参数的函数,它只会在它收到的任何内容上加 2。所以let f = g (+) 2 in f 10 等于 12,let f = g (*) 5 in f 5 等于 25。
(另见my explanation of HOFs,以Scheme为例。)
理解柯里化
Currying 是一种将多个参数的函数转换为 1 个参数的函数的技术,该函数返回一个具有 1 个参数的函数,该函数返回一个具有 1 个参数的函数......直到它返回一个值。这比听起来容易,例如我们有一个有 2 个参数的函数,例如 (+)。
现在想象一下,你可以只给它一个参数,它会返回一个函数?您可以稍后使用此函数将这个现在包含在这个新函数中的 1st 参数添加到其他内容中。例如:
f n = (\m -> n - m)
g = f 10
g 8 -- would return 2
g 4 -- would return 6
猜猜看,Haskell 默认对所有函数进行柯里化。从技术上讲,Haskell 中没有多参数的函数,只有一个参数的函数,其中一些可能返回一个参数的新函数。
从类型上可以看出。在解释器中写入:t (++),其中(++)是将2个字符串连接在一起的函数,它将返回(++) :: [a] -> [a] -> [a]。类型不是[a],[a] -> [a],而是[a] -> [a] -> [a],也就是说(++)接受一个列表,返回一个[a] -> [a]类型的函数。这个新函数可以接受另一个列表,它最终会返回一个 [a] 类型的新列表。
这就是为什么 Haskell 中的函数应用程序语法没有括号和逗号的原因,将 Haskell 的 f a b c 与 Python 或 Java 的 f(a, b, c) 进行比较。这不是什么奇怪的审美决定,在 Haskell 函数应用程序中从左到右,所以 f a b c 实际上是 (((f a) b) c),这完全有道理,一旦你知道 f 是默认柯里化的。
然而,在类型中,关联是从右到左的,所以[a] -> [a] -> [a] 等价于[a] -> ([a] -> [a])。它们在 Haskell 中是一样的,Haskell 对待它们完全相同。这是有道理的,因为当你只应用一个参数时,你会得到一个 [a] -> [a] 类型的函数。
另一方面,检查map:(a -> b) -> [a] -> [b]的类型,它接收一个函数作为它的第一个参数,这就是它有括号的原因。
要真正深入了解柯里化的概念,请尝试在解释器中找到以下表达式的类型:
(+)
(+) 2
(+) 2 3
map
map (\x -> head x)
map (\x -> head x) ["conscience", "do", "cost"]
map head
map head ["conscience", "do", "cost"]
部分应用和部分
现在您了解了 HOF 和柯里化,Haskell 为您提供了一些语法来缩短代码。当你调用一个带有 1 个或多个参数的函数来取回一个仍然接受参数的函数时,它被称为 partial application。
您已经了解,您可以只部分应用函数,而不是创建匿名函数,因此您可以编写 (replicate 3),而不是编写 (\x -> replicate 3 x)。但是如果你想要一个除法 (/) 运算符而不是 replicate 怎么办?对于中缀函数,Haskell 允许您使用任一参数部分应用它。
这称为sections:(2/) 等价于(\x -> 2 / x),(/2) 等价于(\x -> x / 2)。使用反引号,您可以获取任何二进制函数的一部分:(2`elem`) 等价于 (\xs -> 2 `elem` xs)。
但是请记住,任何函数在 Haskell 中默认情况下都是柯里化的,因此总是接受一个参数,因此节实际上可以与任何函数一起使用:让 (+^) 是一个奇怪的函数,它对 4 个参数求和,然后 let (+^) a b c d = a + b + c in (2+^) 3 4 5 返回 14 .
作曲
编写简洁灵活的代码的其他便捷工具是composition 和application operator。组合运算符(.) 将函数链接在一起。应用运算符($) 只是将左侧的函数应用于右侧的参数,因此f $ x 等价于f x。但是($) 在所有运算符中的优先级最低,所以我们可以用它来去掉括号:f (g x y) 等价于f $ g x y。
当我们需要对同一个参数应用多个函数时,它也很有帮助:map ($2) [(2+), (10-), (20/)] 将产生 [4,8,10]。 (f . g . h) (x + y + z)、f (g (h (x + y + z)))、f $ g $ h $ x + y + z 和 f . g . h $ x + y + z 是等价的,但 (.) 和 ($) 是不同的东西,所以请阅读 Haskell: difference between . (dot) and $ (dollar sign) 和 parts from Learn You a Haskell 以了解区别。