让我们从一个例子开始:在 Advent of Code 2022 day 1 中,我们有一组数字的字符串,看起来像:
1000 |
我们想写一个程序,对每组进行求和,并返回最大的一组。本例中最大的一组是第4组,24000。
下面是一个命令式的解决方案:
const input = $('pre').innerText |
而这里是一个函数性的解决方案:
const input = $('pre').innerText |
命令式的解决方案:
- 我们声明了名为runningMax和runningSum的变量,并在整个程序中对其进行变异。
- 我们使用for循环来进行迭代
函数式解决方案:
- 我们避免声明易变的变量
- 我们不使用for循环,而是使用map、reduce和sort等高阶函数进行迭代
高阶函数是指对其他函数进行操作的函数。map、reduce和sort以一个函数作为参数。
究竟如何进行函数式编程?
避免:
- 可变状态
- 迭代步骤
- 循环语句
- 数据和行为交织在一起
偏爱:
- 函数参数和不可变的数据
- 用传送数据替代迭代步骤
- 用递归或高阶函数替代循环
- 数据和行为分离
看了上面的两个例子,你可能认为函数式编程就是简单地使用高级函数。
也许你会想,"map只是一个for循环!"。
如果map并不存在呢?"一个函数式程序员会自己用递归来写map。
任何用循环语句写的东西也可以用递归来写。
想象一下,我们想要一个范围函数,range(5)返回[1,2,3,4,5]。
命令式:
function range(end) { |
函数式:
function range(end, cur=[]) { |
在我开始探索函数式编程之前,我从未想过以递归方式实现这个函数。
但现在我比较这两种解决方案,递归的解决方案实际上感觉更自然。
为什么使用函数式编程
函数式编程有很多惊人的好处。让我们来探讨一下最大的东西。
1、避免与易变状态有关的bug:
就像静态类型消除了由动态类型引起的错误一样,函数式编程也消除了由可变状态引起的错误。每个程序员都曾复制过一个数组,对其进行了修改,然后花了太多的时间去弄清楚原来的数组是如何被修改的(又称浅层拷贝与深层拷贝)。当可改变的状态在代码中被共享时,出现这样的错误的可能性就会增加。
2、编写声明性代码:
命令式代码描述了如何做事情,而声明式代码则描述了我们正在做什么。这意味着声明性代码通常更容易被理解。想一想原来的命令式解决方案中的变量是如何命名runningMax和runningSum的。我们很想把这些变量命名为maxGroup和sum,但这是在撒谎。它们从0开始,在程序的过程中不断变化。我们被困在描述我们如何做事情的变量名称中。在函数式解决方案中,sortedGroups准确描述了我们正在做的事情。
3、编写更容易测试的代码:
如果你习惯于写命令式代码,那么写无效函数可能是很诱人的,因为它在函数的范围之外突变变量。做游戏的一个常见方法是将状态声明为变量,然后写一个无效函数,如update(),在每个tick上突变这些变量。函数式编程倾向于将游戏状态作为参数进行更新并返回新的状态,而不是变异变量。现在我们的代码更容易测试了。我们只需用一些游戏状态调用update,并断言其结果是我们所期望的。