上篇文章我们从全局宏观的角度,了解了 Haskell 语言的特性和学习方法,这篇文章我们来谈一个具体的函数式语言特性:高阶函数。

我选择这个问题,一个重要的原因是许多其他范式的语言纷纷在新版本、新标准中引入了高阶函数特性。在更纯粹的函数式语境下理解这个概念设计的必要性,我觉得对于我们更好使用高阶函数是有帮助的。

换言之,哪怕你以后再也不碰 Haskell,了解一下高阶函数绝对不亏


1. 为什么需要高阶函数?

我们先来看一个简单的例子:假设我们有一个由整数构成的列表,现在想要得到一个新列表,新列表的每个元素是原列表的两倍。

如果使用命令式的思路来解决这个问题,常见的做法是使用 for 循环。我们来看一个 Python 的示例代码:

def double_list(num_list: list[int]) -> list[int]:
    result = []
    for i in num_list:
        result.append(i * 2)
    return result

print(double_list([1, 2, 3, 4, 5]))
# [2, 4, 6, 8, 10]

在这个简单的需求下,命令式的写法其实也是很直观的。只需要在 for 循环中,每步向 result 列表添加计算好的新元素,就可以得到正确结果。

如果我们希望在函数式语言下达到类似的效果,又应该怎么办呢?

由于在纯函数式语言 (如 Haskell) 中,我们要避免使用可变状态,无法在函数式语言使用类似 result 这种可变的变量。换言之,函数式语言中我们不能用传统的、基于可变状态的 for 循环来解决这个问题。

要想实现类似效果,一个自然的想法就是要把函数式语言中的“一等公民”,也就是函数,将其作用发挥到极致

我们可不可以通过函数的组合,来代替 for 循环呢?这时,我们就需要引入这篇文章的主角:高阶函数。


2. 高阶函数 map

什么是高阶函数 (Higher-Order Function) 呢?我们一般认为,可以将其他函数作为一个函数的参数传入进去,或者返回值是一个函数的函数可以被称之为高阶函数。

在上述这个案例中,Haskell 语言的常见做法是使用 map 这个高阶函数。

比如,我们可以通过定义一个 doubleNum 函数,将这个函数传入 map 的方式来解决这个问题:

doubleNum :: Int -> Int
doubleNum x = x * 2

result :: [Int]
result = map doubleNum [1, 2, 3, 4, 5]
-- [2, 4, 6, 8, 10]

这里 map 的原理是什么呢?首先,我们定义了一个可以把任意整数翻倍的函数;然后,我们通过 map 高阶函数,把这个 doubleNum 函数应用到了列表中的每一个值;最终得到了预期的计算结果。

在这里,map 函数接受一个函数和一个列表,返回一个新列表。新列表中的每个元素,都是原列表对应元素经过传入函数计算后的结果。

Map 高阶函数示意图

实际上,这个思路和命令式是类似的,但是使用的工具不同。这里的 map 是一个纯粹的函数,唯一特殊的地方,就是接受第一个参数实际上也是一个函数。

当然,如果你觉得这样写有点麻烦,我们实际上可以使用「匿名函数」这个特性来进一步简化代码:

result :: [Int]
result = map (\x -> x * 2) [1, 2, 3, 4, 5]

匿名函数允许我们在不给函数起名字的情况下,使用这个函数。这在一些函数特别简单的场景下很有用,比如这里把值翻倍这种简单的需求,用匿名函数可以简化代码。

这里的 \x -> x * 2,本质上和上文中起名的函数作用是一致的。


3. 进一步讨论

通过上面的例子,我想大家对高阶函数的一些常见使用场景有了进一步的理解。

实际上,尽管提到高阶函数,大家一般会想起函数式语言中特别常用的 map\filter\foldl\foldr,开发者完全可以根据自己的需求,自由拼装组合各种函数,灵活实现自己的需求

当需求变得更复杂的时候,我们会发现使用高阶函数的代码往往比传统 for 循环的代码更具表达力,而纯函数特性又会使高阶函数具备行为确定性、可测试性

当然,上文提到的 Haskell 例子就是最简略的写法了吗?其实不是。我们还可以这么写:

result :: [Int]
result = map (*2) [1, 2, 3, 4, 5]

至于为什么「*2」这也可以是一个函数,就不得不提到函数式语言的另外两个特性:柯里化部分应用。这些概念,我将会在本专栏下一篇文章详细说明。

当然,即便到这里,我想我们仍然不能说这种高阶函数的写法就一定比 for 循环更出色。

但如果我们继续深入学习 Haskell 的知识,在掌握 Functor 概念后,让 map 推广到 fmap,函数式写法的优雅性才会真正展现。当然,这些内容就要在专栏更靠后的文章才会讨论了。


在文章最后,我还是想再强调一遍:我们不是只能在函数式语言中才可以使用高阶函数

比如,开头用 Python 写的代码片段,我们完全可以使用函数式风格写法实现:

def double_list(num_list: list[int]) -> list[int]:
    return list(map(lambda x: x * 2, num_list))

print(double_list([1, 2, 3, 4, 5]))

我们同样可以使用 Python 内置的 map 函数来完成这个任务。这里的 lambda x: x * 2 是和上文类似的匿名函数。

尽管高阶函数写法在 Python 中并非强制的实现方式,我想掌握更多的解决问题工具,对于理解别人的代码、写出表达能力更强的代码都是有帮助的。

如果你喜欢这篇博客,欢迎关注我。共勉!

本系列的下一篇文章:函数式心法 (3):巧用柯里化和部分应用