函数式编程

71 阅读8分钟

什么是函数式编程

函数式编程是一种编程范式、一种方法论,主要的编程范式还有命令式编程声明式编程

相较于命令式编程,函数式编程着眼点是函数,而非过程函数本质上是一种数据关系的映射,输入通过函数都会返回有且只有一个输出值。而这种映射关系也是可以组合的,一旦我们知道一个函数的输出类型可以匹配另一个函数的输入,那他们就可以进行组合。

因此函数式编程强调的是如何通过函数的组合转化构建关系并解决问题,而不是通过写什么语句来解决问题。当你的代码越来越多的时候,这种函数的拆分组合会产生出强大的生命力。

核心概念

函数式编程的核心概念有两个:

  • 数据不可变:  它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
  • 无状态:  主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。

为了实现这个目标,函数式编程提出纯函数这个概念。即纯函数=无状态+数据不可变

纯函数

在 Redux 的三大原则中,我们看到,它要求所有的修改必须使用纯函数。

Changes are made with pure functions

其实纯函数的概念很简单就是两点:

  • 不依赖外部状态(无状态):  函数的的运行结果不依赖全局变量,this 指针,IO 操作等。
  • 没有副作用(数据不变):  不修改全局变量,不修改入参。

所以纯函数才是真正意义上的 “函数”, 它意味着相同的输入,永远会得到相同的输出

我们这么强调使用纯函数纯函数的优势有哪些呢?

  • 可测试性:由于纯函数的输出完全由输入决定,所以相同的输入总是会得到相同的输出,这使得它们的行为非常可预测和可靠。测试纯函数非常简单,只需要给定输入,验证输出是否正确即可。不需要考虑外部环境和状态的变化,测试用例可以非常独立和自包含。
  • 可缓存性:因为纯函数对于相同的输入总是返回相同的输出,所以可以将其结果缓存起来,以避免重复计算。通过缓存纯函数的结果(例如 Memoization 技术),可以显著提高性能,尤其是对于计算量大的函数调用。
  • 可复用性:由于纯函数不依赖外部状态,并且没有副作用,只依赖输入参数来计算输出,因此它们可以在不同的上下文中被重复使用,减少了重复代码的数量
  • 引用透明:一个表达式在程序中任何地方被替换为它的值时,程序的行为和结果不会发生变化。引用透明的特性直接关系到纯函数和函数式编程的许多优点,如可预测性、可测试性和易于推理。
  • 并行代码:纯函数没有副作用,不会改变外部状态,因此在并行或并发环境下运行时不会引起竞态条件或数据冲突。所以可以安全地并行或并发执行,从而提高程序的性能和效率。

两种操作

如果说函数式编程中有两种操作是必不可少的那无疑就是柯里化(Currying)函数组合(Compose) ,柯里化其实就是流水线上的单元加工站,函数组合就是我们的流水线,它由多个加工站组成。

柯里化——单元加工站

柯里化是把一个多参数函数转化成一个嵌套的一元函数的过程。

f(a,b,c) → f(a)(b)(c)

我们尝试写一个 curry 版本的 add 函数

//比较容易读懂的ES5写法
var add = function(x) {
  return function(y) {
    return x + y;
  }; 
};

//ES6写法,也是比较正统的函数式写法
var add = x => (y => x + y);

const increment = add(1);

increment(10); // 11

为什么这个单元函数很重要?

还记得我们之前说过的,函数的返回值,有且只有一个嘛?  如果我们想顺利的组装流水线,那我就必须保证我每个加工站的输出刚好能流向下个工作站的输入。因此,在流水线上的加工站必须都是单元函数。

现在很好理解为什么柯里化配合函数组合有奇效了,因为柯里化处理的结果刚好就是单输入的。

部分函数应用 vs 柯里化

经常有人搞不清柯里化和部分函数应用 ( Partial Function Application ),经常把他们混为一谈,其实这是不对的,在维基百科里有明确的定义,部分函数应用强调的是固定一定的参数,返回一个更小元的函数。通过以下表达式展示出来就明显了:

// 柯里化
f(a,b,c) → f(a)(b)(c)
// 部分函数调用
f(a,b,c) → f(a)(b,c) / f(a,b)(c)

柯里化强调的是生成单元函数部分函数应用的强调的固定任意元参数,而我们平时生活中常用的其实是部分函数应用,这样的好处是可以固定参数,降低函数通用性,提高函数的适合用性,能够⼤量减少样板⽂件代码(boilerplate code)。

// 假设一个通用的请求 API
const request = (type, url, options) => ...
// GET 请求
request('GET', 'http://....')
// POST 请求
request('POST', 'http://....')

// 但是通过部分调用后,我们可以抽出特定 type 的 request
const get = request('GET');
get('http://', {..})

高级柯里化

通常我们不会自己去写 curry 函数,现成的库大多都提供了 curry 函数的实现,但是使用过的人肯定有会有疑问,我们使用的 Lodash,Ramda 这些库中实现的 curry 函数的行为好像和柯里化不太一样呢,他们实现的好像是部分函数应用呢?

const add = R.curry((x, y, z) =>  x + y + z);
const add7 = add(7);
add7(1,2) // 10
const add1_2 = add(1,2);
add1_2(7) // 10

其实,这些库中的 curry 函数都做了很多优化,导致这些库中实现的柯里化其实不是纯粹的柯里化,我们可以把他们理解为“高级柯里化”。这些版本实现可以根据你输入的参数个数,返回一个柯里化函数/结果值。即,如果你给的参数个数满足了函数条件,则返回值。这样可以解决一个问题,就是如果一个函数是多输入,就可以避免使用 (a)(b)(c) 这种形式传参了。

所以上面的 add7(1, 2) 能直接输出结果不是因为 add(7) 返回了一个接受 2 个参数的函数,而是你刚好传了 2 个参数,满足了所有参数,因此给你计算了结果,下面的代码就很明显了:

const add = R.curry((x, y, z) =>  x + y + z);
const add7 = add(7);
add(7)(1) // function

如果 add7 是一个接受 2 个参数的函数,那么 add7(1) 就不应该返回一个 function 而是一个值了。

因此,记住这句话:我们可以用高级柯里化去实现部分函数应用,但是柯里化不等于部分函数应用

柯里化的应用

通常,我们在实践中使用柯里化都是为了把某个函数变得单值化,这样可以增加函数的多样性,使得其适用性更强,这⼀点同样适⽤于其他的⾼阶函数( higher order function )(⾼阶函数:参数或返回值为函数的函数)。

const replace = curry((a, b, str) => str.replace(a, b));
const replaceSpaceWith = replace(/\s*/);
const replaceSpaceWithComma = replaceSpaceWith(',');
const replaceSpaceWithDash = replaceSpaceWith('-');

通过上面这种方式,我们从一个 replace 函数中产生很多新函数,可以在各种场合进行使用。事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法。

更重要的是,单值函数是我们即将讲到的函数组合的基础

函数组合

学会了使用纯函数以及如何把它柯里化之后,我们会很容易写出这样的“包菜式”代码:

h(g(f(x)));

虽然这也是函数式的代码,但它依然存在某种意义上的“不优雅”。为了解决函数嵌套的问题,我们需要用到“函数组合”:

//两个函数的组合
var compose = function(f, g) {
    return function(x) {
        return f(g(x));
    };
};

//或者
var compose = (f, g) => (x => f(g(x)));

var add1 = x => x + 1;
var mul5 = x => x * 5;

compose(mul5, add1)(2);
// =>15 

我们定义的compose就像双面胶一样,可以把任何两个纯函数结合到一起。当然你也可以扩展出组合三个函数的“三面胶”,甚至“四面胶”“N面胶”。

这种灵活的组合可以让我们像拼积木一样来组合函数式的代码:

var first = arr => arr[0];
var reverse = arr => arr.reverse();

var last = compose(first, reverse);

last([1,2,3,4,5]);
// =>5

参考文章: zhuanlan.zhihu.com/p/81302150 zhuanlan.zhihu.com/p/21714695