函数式编程介绍

原文信息: 查看原文查看原文

Introduction to Functional Programming

- Dylan Paulus

这篇文章来源于一个我参加Boise Code Camp 2019时的一个比较命令式编程和函数式编程解决问题的例子。目的不是教授完整的函数式编程,而是介绍一种不同于传统方法(循环、转换等)的思维方式。当遇到问题时,拥有不同的参考框架作为工具箱添加更多的工具。

函数式编程的基础可以分为三个主要概念:

  • 不可变数据结构
  • 纯函数
  • 头等函数

让我们快速了解一下这些要点的含义。

不可变数据结构

当和像JavaScript的编程语言工作时,我们可以将数据分配给变量let myVariable = 5;。但是,没有什么能阻止稍后在myVariable = "Now I'm a string."。这可能很危险——也许另一个函数依赖于myVariable作为一个数字,或者如果一些异步函数同时依赖于myVariable工作!我们可能会遇到合并冲突。

示例:

const obj = {
  a: 1,
  b: 2
};

function addOne(input) {
  return {
    a: input.a + 1,
    b: input.b + 1
  };
}

const newObj = addOne(obj);

newObj === obj; // false

纯函数

纯函数是无副作用的。这意味着什么?好吧,一个只根据输入来计算输出的函数被当做一个纯函数。如果我们的函数接受一个输入,执行数据库更新,然后返回一个值,那么我们的代码中包含一个副作用——更新数据库。多次调用函数可能不会经常返回相同的结果(内存溢出、数据库被锁等)。拥有纯函数对于帮助我们编写无缺陷、易于测试的代码至关重要。

示例:

// 非纯函数相加
function notPureAdd(a, b) {
  return a + new Date().getMilliseconds();
}

// 纯函数相加
function pureAdd(a, b) {
  return a + b;
}

头等函数

术语"头等函数"可能看起来很奇怪,但是它全部的含义是可以像使用其他数据类型一样传递和使用函数。比如:string,int,float等。支持"头等函数"的编程语言让我们向其他函数传递函数。把它想象成依赖注入。如果你已经在工作的任何地方使用JavaScript的头等函数,那我们将接下来的例子中接触更多。

示例:

// robot 想要传入一个 function 
function robot(voiceBox) {
  return voiceBox("bzzzz");
}

// console.log is a function that logs to the console
robot(console.log);
// alert is a function that shows a dialog box
robot(alert);

比较命令式编程和函数式编程

为了展示命令式编程和函数式编程的基本比较,让我们对一个数组[1, 2, 3, 4]中的数字加到一起,获得它们的合。

我们可以命令式地像下面这样写一些:

const list = [1, 2, 3, 4];

let sum = 0;

for (let i = 0; i < list.length; i++) {
  sum += list[i];
}

console.log(sum); // 10

将这个转换成一个函数的方式,我们有一个大问题。在列表的每次迭代中,我们都会改变sum的值。记住。。。不可变数据结构。

为了让这个代码正常工作,我们来分解求和的计算方式。

首先,我们从某个值开始,在我们的例子中是0(见let sum = 0;这一行)!接下来,我们拉出数组中的第一项1,并将其添加到我们的sum中。现在,我们有0 + 1 = 1。然后我们重复这一步,拉出2并加到sum1 + 2 = 3。继续这样直到我们遍历数组的长度。

以不同的方式将内容可视化:

0 + [1,2,3,4]
0 + 1 + [2,3,4]
1 + 2 + [3,4]
3 + 3 + [4]
6 + 4
10

我们可以将这个算法看作两个独立的函数,首先我们需要某种方法将数字相加。

function add(a, b) {
  return a + b;
}

简单!

接下来,我们需要一个方法来循环遍历给定的数组。由于大多数函数式编程通常依赖于递归而不是循环,因此我们将创建一个递归函数,该函数将循环遍历数组。让我们看看可能是什么样子。

function loop(list, index = 0) {
  if (!list || index > list.length - 1) {
    // We're at the end of the list
    return;
  }

  return loop(list, index + 1);
}

在这个函数中,我们获取要循环遍历的列表,以及一个索引,我们将使用它来确定当前列表中的位置。如果我们到达了列表的末尾,或者给出了一个无效的列表,那么我们就完成了循环。如果没有,我们将再次调用循环,添加索引。尝试在循环函数内部return loop(list, index + 1)之前添加一个console.log(list[index])。我们应该看到1 2 3 4打印到控制台上!

最后为了计算我们的数组,我们将需要合并loopadd函数。在阅读本例时,请记住上面的算法:

function loop(list, accu = 0, index = 0) {
  if (!list || index > list.length - 1) {
    return accu;
  }

  const result = add(accu, list[index]);

  return loop(list, result, index + 1);
}

我们在loop函数中重新排列了一些参数。现在我们有一个accu参数(积累),它将追踪列表中给定位置的合。我们也直接用add函数来获取添加到当前项accu的结果。如果我们console.log(loop(list));,我们应该将结果10打印到控制台!

我们再往前走一步怎么样?如果我们不想把数字列表求和,而是把它们相乘呢?目前,我们必须复制loop函数,黏贴它,然后将add函数更改为其他东西(可能multiply(相乘)?)。多么痛苦啊!还记得头等函数吗?我们可以在这里使用这个想法来试代码更通用。

function loop(func, list, accu = 0, index = 0) {
  if (!list || index > list.length - 1) {
    return accu;
  }

  const result = func(accu, list[index]);

  return loop(func, list, result, index + 1);
}

在上面的例子中,唯一改变的东西是我们现在给loop添加了一个函数当做新的参数。我们将调用传入的函数来获取结果,而不是add。现在我们可以很简单地对列表进行addmultiplysubtract等。

  • loop(add, list)
  • loop(function(a, b) { return a * b; }, list)

我们再也不只是循环遍历数组,而是像折叠纸一样折叠数组,直到得到一个结果。在函数式编程中,这种函数可以被称为fold,且在JavaScript中,我们将它看作reduce

function reduce(func, list, accu = 0, index = 0) {
  if (!list || index > list.length - 1) {
    return accu;
  }

  const result = func(accu, list[index]);

  return reduce(func, list, result, index + 1);
}

结尾

我们研究了函数式编程的基础知识,以及如何分解一个问题,从而为同一个问题提供不同的解决方案。reduce被视为其它操作的基础,比如map()filter()。这是我为你做的测试,我们如何只使用刚才创建的reduce()来实现这两个函数?

提示

记得reduce的算法吗?

0 + [1,2,3,4]
0 + 1 + [2,3,4]
1 + 2 + [3,4]
3 + 3 + [4]
6 + 4
10

如果不是从0开始,而是从一个[]数组开始呢?

答案

分享于 2020-01-07

访问量 990

预览图片