如何使用偏函数技术提高代码的可读性

2023-2 review : (事务数据类的)应用程序 达到一定规模一定会出现大量重复,这种重复不但出现在带状态的类对象(OO技术),也会出现在无状态的纯函数。偏函数(或叫函数参数部分固化)和柯里化就是一种 应对工程规模化的FP技术

对刚接触JS的人来说,「偏函数」是一个让人困惑的概念,其实「偏函数」指代的不是一个特殊功能的函数,而是 一种有目的函数式编程,这个目的可概述为,将一个目标函数(假如叫 Fo)“偏化”(什么叫偏化,本文后面会讲),简化对Fo的调用(Fo功能的使用),从而提高代码可读性,和可维护性。

偏函数正名 是 函数参数的局部应用化(partial application)。与偏函数一样高度相关的,同样让新手困惑的是,柯里化函数,柯里化是全应用化,是通例,偏化是特例。

理解函数偏化和柯里化的实质 需要很多基础,函数偏化是柯里化的理解基础,而「高阶函数」是它们的基础,更基础的是「函数式编程」,当然还有一个终极的基础,「编程」是什么。

函数式编程 是一个很大的题目,不容易讲清楚;「编写高阶函数」是 函数式编程 的重要特征,同样高阶函数也需要很多解释。但是,我们可以从以他们为基础的应用技术——函数偏化技术——的实例,比较具体的了解函数式编程的实质和意义。

关于偏函数技术的应用例子,我自己不写,略译此文《How to use partial application to improve your JavaScript code》作为引子。

How to use partial application to improve your JavaScript code

函数式技术能提高代码优雅性

函数式编程技术能 改善我们的代码的质量,其中一项常用技术就是,函数偏化(partial application)。函数偏化一开始理解比较费解,学会之后可使用它降低代码冗余,从而提高代码可维护质量。

什么是函数偏化?

函数偏化或者函数局部应用(partial application)注1是指,对一个函数Fo的 部分形式参数进行固化处理(简称偏化) 而生成一个新函数 Fp,负责处理偏化的函数叫偏化处理函数 partialF。新函数 Fp功能变得具体,输入参数变少,从而提高代码维护质量。

从这个定义可知,偏函数的技术不只是一个单一函数,它涉及了三个函数:原初函数Fo,偏化处理函数partialF和新函数 Fp,当然为了方便,我们也可简单称偏化处理后的新函数 Fp 为偏函数。

被“偏化”的原初函数Fo

我们来看一个可以被“偏化”的函数:

const list = (lastJoin, ...items) => {
  const commaSeparated = items.slice(0,-1).join(", ");
  const lastItem = items.pop();
  return `${commaSeparated} ${lastJoin} ${lastItem}`;
}

这是一个功能很简单的函数,可叫罗列 list——将输入的数据格式化后罗列出来,它有两个参数:

  1. lastJoin 一个单词,连接最后一项,定义连接的类型;
  2. items,待输出的任意项参数,使用了扩展运算符,将收集到的参数组成数组;

功能实现过程:

  1. 将输入参数数组除了后一项 转化为字符串,使用逗号分隔,保存在commaSeparated;
  2. 取出最后一项 lastItem
  3. 使用字符号模板 格式化输出并返回给调用者

下面是一些测试的效果:

list("and", "red", "green", "blue"); // "red, green and blue"
list("with", "red", "green", "blue"); // "red, green with blue"
list("or", "red", "green", "blue"); // "red, green or blue"

“偏化”的需求

我们可以在 code base 中任意使用 list 函数的功能,如果code base出现大量某个类型连接(and”, “with”, “or” )罗列输出功能,可能会造成阅读噪音,我们可不可省掉输入lastJoin这个参数呢注2

例如 我们将 lastJoin 固定为 “and”生成一个新的函数 listAnd,使用时可省掉一个参数:

listAnd("red", "green", "blue"); // "red, green and blue"

如何做到呢?这个时候 偏函数技术派上用场了。

“偏化”处理函数partialF

函数偏化的需求和意义还是比较直观的,但是对于缺乏函数式编程经验的人来说,固定一个参数如何可能?!在JS里,函数可以作值处理,这就是可能的基础。

partialF是一高阶函数,属于改造现有函数功能值的一类(接受一个函数值参数,并返回一个新函数)。高阶函数的任务是 编辑函数功能值,例如本例是将list改造为listAnd,增加了功能值具体涵义(添加连接类型and);

初步分析,这个功能编辑的需求,不需对原功能(值)有所增删,只是使用上(调用)为了便利作简单的微调——固定一个参数;所以新函数不需增加什么功能语句,只需要 提供固定参数去调用原功能函数 即可。所以实现如下:

const partialF = (fn, firstArg) => {
  return (...lastArgs) => {
    return fn(firstArg, ...lastArgs);
 }
}

注意这里的偏化处理函数是不是通用的,它假定只固定第一个参数,如果要固定任意位置和数量的参数则不适用。

生成的新偏函数Fp

现在我们可以执行“偏化”处理:

const listAnd = partialF(list, "and");

我们现在已经创建了一个偏函数。我们可以在我们的程序中一次又一次地使用这个专门的功能:

listAnd("red", "green", "blue");    // "red, green and blue"

“标准的”的偏化处理函数

这里的partialF其实是演示性质的,事实上,在一些流行的 函数式代码库中已经集成这个工具函数,例如Ramda JS

bind函数也是偏化处理函数

值得一提的是,即使你是第一次接触偏函数技术,但极有可能你已经使用过它。原因是,如果你使用过 .bind() 函数,它的实质其实就是一个参数偏化的应用例子,只是偏化不是形式参数,而是this。将this固定,然后返回一个新函数,本质和 partialF是一样的。

小结

以上整个例子,最困难的部分是,编写偏化处理函数这个高阶函数。高阶函数就像功能改造机,它的第一目的是生产需要的功能,其次才是改善可读性;可用性,比可读性优先一些。

函数功能值 的 偏化处理 只是函数式编程的一种,另外一种是柯里化处理。

参考


  1. 偏函数原义是partial application,这是个简称,全称应该叫「局部参数应用化」。所谓application 就是给参数提供具体的值,所以偏化也可翻译固化。
  2. 这里例子不是很有代表性,我们在实践中只要注意,任何函数的参数都是泛化的,固化任意一个参数都可将函数功能具体化。
裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.