理解高阶函数
Haskell 作为一种函数式语言,支持高阶函数(HOF)。在数学中,HOF 被称为泛函,但您不需要任何数学来理解它们。在通常的命令式编程中,就像在 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
,适用f
于m
和n
。基本上,当我们调用时,g (+) 2
我们创建一个有一个参数的函数,它只会在它收到的任何内容上加 2。所以let f = g (+) 2 in f 10
等于 12 和let f = g (*) 5 in f 5
等于 25。
(另请参阅我以 Scheme 为例对 HOF 的解释。)
理解柯里化
柯里化是一种将多个参数的函数转换为 1 个参数的函数的技术,该函数返回一个具有 1 个参数的函数,该函数返回一个具有 1 个参数的函数......直到它返回一个值。这比听起来容易,例如我们有一个有 2 个参数的函数,比如(+)
.
现在想象一下,你可以只给它一个参数,它会返回一个函数?您可以稍后使用此函数将现在包含在此新函数中的第一个参数添加到其他内容中。例如:
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]
。这个新函数可以接受另一个列表,它最终会返回一个新的 type 列表[a]
。
这就是为什么 Haskell 中的函数应用程序语法没有括号和逗号,比较 Haskellf 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 对待它们完全相同。这是有道理的,因为当你只应用一个参数时,你会得到一个 type 的函数[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 个或多个参数的函数来取回一个仍然接受参数的函数时,它被称为部分应用程序。
您已经明白,您可以部分应用一个函数,而不是创建匿名函数,因此(\x -> replicate 3 x)
您可以只编写(replicate 3)
. 但是如果你想要一个除法(/)
运算符而不是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。
作曲
编写简洁灵活的代码的其他方便工具是组合和应用程序运算符。组合运算符(.)
将功能链接在一起。应用运算符($)
只是将左侧的函数应用于右侧的参数,因此f $ x
等效于f x
. 然而($)
在所有运算符中具有最低的优先级,所以我们可以用它来去掉括号:f (g x y)
相当于f $ g x y
.
当我们需要对同一个参数应用多个函数时,它也很有帮助:map ($2) [(2+), (10-), (20/)]
将 yield [4,8,10]
。(f . g . h) (x + y + z)
, f (g (h (x + y + z)))
, f $ g $ h $ x + y + z
andf . g . h $ x + y + z
是等价的,但是(.)
and($)
是不同的东西,所以请阅读Haskell: 之间的区别。(点)和 $(美元符号)以及Learn You a Haskell中的部分内容以了解差异。