【译】如何在 JavaScript 中使用强大的复合函数

原文:How to use powerful function composition in Javascriptjavascript

声明:翻译原文从国外知名博客网站上获取,并利用业余时间进行翻译。若是发现网络上有其余译文,多是由于开始翻译时没有发现已存在译文或是感受原译文翻译质量不佳而从新翻译。不论出于哪类缘由,本译文不会包含任何抄袭行为。java


复合函数(Function composition) 是 JavaScript 编程中在面向对象和函数式编程两者之间至关大的一个差别。程序员

本文会解释类层级(Class Hierarchy)与复合函数之间的区别,以及在代码中利用复合函数和函数式编程优势的示例。es6

类层级与「机器狗」

在面对对象编程中,定义 Class。编程

例如,你定义了父类 Animal 并拥有一个 move 方法,并继续建立 CatDog 类从 Animal 继承 move 方法,并添加本身的方法 bark (狗叫)和 meow(猫叫)。数组

而后,你又定义了一个 Robot 类拥有方法 chargeBattery网络

如今,若是你想建立一个须要 movechargeBattery 方法的 RoboDog 类,以及一个为 Dog 加强 barkroboBark,那么要怎么办呢? 这个类须要从 DogRobot 同时继承,但 JavaScript 却不容许这样作。架构

为了解决这个问题以及其余一些问题,在面向对象编程中再也不推荐使用继承。 相反,咱们须要为类定义一个接口(当前不存在于 JavaScript 中),并实例化继承的类并将它们用做依赖项。app

此外,依赖项应该经过依赖注入来处理,以提升可测试性和灵活性,详情可参阅: JavaScript Pure Functions for OOP developersless

RoboDog 类看起来像下面这样:

import {Animal, Dog} from './animals';
import {Robot} from './robots';
​
class RoboDog {
  constructor(animal, dog, robot) {
    this.animal = new animal();
    this.dog = new dog();
    this.robot = new robot();
  }
  move() {
    return this.animal.move();
  }
  bark() {
    return this.dog.bark();
  }
  chargeBattery() {
    return this.robot.chargeBattery();
  }
  roboBark() {
    return 2 * this.dog.bark();
  }
}
​
const roboDog = new RoboDog(Animal, Dog, Robot);
roboDog.roboBark();
复制代码

复合函数

复合函数基于一元柯西化Monadic Curried)的使用和优选纯函数Pure Function)。

// 一元函数只接受一个参数
const monadic = one => one + 1;
​
// 这不是一元函数
const notMonadic = (one, two) => one + two;
​
// 这是柯西、一元、高阶函数
const curry = one => two => one + two;
复制代码

复合函数很是简单,它使用多个函数,而且每一个函数接收输入,并将其输出移交给下一个函数:

const plusOne = a => a + 1;
const plusTwo = a => a + 2;
​
const composedPlusThree = a => plusTwo(plusOne(a));
​
composedPlusThree(3); // 6
复制代码

在函数式编程中,你定义的是表达式而不是语句,函数也只是一个表达式。所以,JavaScript 支持将函数做为参数,或把返回的函数做为其输出的高阶函数。

为了让其变得容易,你能够定义高阶函数 composecomposePipe

const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const composePipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
复制代码

compose 和 composePipe 在组合函数的顺序上有所不一样:

const plusA = s => s + 'A';
const plusB = s => s + 'B';
​
const composed1 = s => compose(plusA, plusB)(s);
const composed2 = s => composePipe(plusA, plusB)(s);
​
composed1(''); // BA
composed2(''); // AB

复制代码

请注意,在这里可使用无参数风格代码(tacit programming 隐式编程 ):

const composedPointFree = compose(plusA, plusB);
​
console.log(composedPointFree('') === composed1('')); // true
复制代码

显然,这是能够的。由于 compose(plusA,plusB) 是一个高阶函数,而 compose 返回一个用于定义新表达式的函数。

若是你使用过Unix,你还能够将函数组合与 Unix 管道相关联,该管道的工做原理相同:$ ls -l | grep key | less

一点点数学

查看上图,你能够看到三个不一样颜色的编号组,它们经过函数 gf 链接。 函数 g 接受参数 Horse 并返回 Horn 。 而后函数 f 接受参数 Horn 并返回 Unicorn。这两个函数的组成是一个函数,而这个函数须要一个 Horse 做为参数,并直接返回一个 Unicorn 做为输出。

由于咱们使用的是纯函数,而且其始终为相同输入返回相同值,因此咱们能够经过一个简单的函数替换组合函数,该函数只须要 Horse 并返回 Unicorn。 这是 Memoization记忆化) 中使用的原则。

函数式编程并不能很好地优化并行处理。正如你所看到的那样,它还拥有容许咱们彻底跳过处理的魔力,并经过跳过它们之间的全部内容来返回问题的答案。

复合函数与「机器狗」

复合函数的使用,实际上与前文中的 RoboDog 面向对象编程实例中所作的,看起来类似。可是,使用复合函数,其函数的构成要优雅得多。

你没有使用类来模拟整个逻辑,而只是定义了表明所需功能的方法。 最终JavaScript 模块的表达以下:

import {bark} from './dog';
import {compose} from './functional';
​
const doubleIt = a => 2 * a;
​
export const roboBark = composePipe(bark, doubleIt);
复制代码

请注意,上面的代码没有引用它不须要的任何内容,这意味着没有提到 AnimalRobot 的功能。 这些并非 RoboDog 独有的,而咱们只想关注一个全新的独特代码。

要使用代码中的全部功能, 你能够自由使用 AnimalDogCatRobotRoboDog 中的功能。

复合函数和对象之间还有另外一个显着差别。 对象保存内部数据和状态,它们是有状态的。 然而,函数式编程中的函数应该是纯粹的和无状态的。

纯函数仅由其输入驱动以提供其输出,它不会改变(变异)任何其余数据,也不会触发任何反作用。 这使得它很是简单、可预测、易于测试,而且易于遵循通用编程的最佳实践。这些都是优秀的程序员应该关心的事情。

在函数式编程中,你应该遵循关注点分离,经过使用控制反转(IOC)的原理和函数式单子(Monads)的方式将任务的执行与其实现分离(IOC 是 AOP 中经常使用概念,Monads 是函数式编程中的概念)。

甚至,若是不使用单子(由于它们的定义会吓到你:A monad is just a monoid in the category ofendofunctors,自函子范畴上的幺半群),你仍然能够解耦代码。只需将功能的定义移动到一个能够集成和提供数据的位置并执行,而后移动到另外一个位置。理想状况下,能够在彻底不一样的模块级别上执行此操做。

作完这些工做,你能够经过单元测试和集成测试来覆盖代码功能。自此,你就能够过上快乐的程序员生活。

拆分你的函数并使用复合函数

你有可能正在使用函数做为可重复的语句序列的盒子,以下所示:

function simonSays(arg) {
  let result = arg.trim();
  result = `Simon Says: ${result}`;
  return result;
}
​
simonSays(' Jump! '); // Simon Says: Jump!
复制代码

上面的函数修剪(trim)字符串参数,修饰它而后返回。 示例上的函数虽然只有五行,但实际上,咱们常常看到由几十行代码表示的函数。

单一职责原则Single Responsibility Principle)规定:每一个函数都应对功能的一部分负责。 这是开放的解释,但咱们能够很容易地发现,上述功能中「修剪」和「装饰」作的是两件事而不是一件事。

让咱们尝试使用 JavaScript 中的复合函数

const trim = a => a.trim();
const add = a => b => a + b;
​
const simonSays = composePipe(trim, add('Simon Says: '));
​
simonSays(' Jump! '); // Simon Says: Jump!
复制代码

使用复合函数,意味着对于程序逻辑的每一步都会有一个可测试且可重用的函数。

测试驱动开发(TDD)要求你,首先为要实现功能的任何部分编写测试用例,而后实现逻辑,并所有经过测试用例的测试。这部分是为了确保程序不会有任何隐藏的、未经测试的逻辑。

经过使用复合函数,你老是能够用一种暴露逻辑并容许轻松测试的方式去编写代码。 更多内容能够查看:Making testable JavaScript code

使用局部应用(Partial Application)建立可重用代码

经过局部应用的柯西化函数来完善上述的 simonSays 函数。局部应用程序意味着你将提供暴露高阶函数中做为基础函数的参数:

const add = a => b => a + b;
const partialSimonSays = add('Simon Says:'); // partial application
const simonSays = composePipe(trim, partialSimonSays);
​
partialSimonSays('Jump!'); // Simon Says: Jump!
simonSays(' Jump! '); // Simon Says: Jump!
复制代码

这容许你建立更多可重用的代码。更多内容能够查看:JavaScript ES6 curry functions with practical examples

探讨你的代码

由于咱们一直在使用纯函数,因此在组合中插入其余函数会很是容易。请参阅下面的示例:

// console.log is impure and does not provide any return value
// so we have to improve it
const investigate = a => console.log(a) || a;
​
const simonSays = composePipe(
  investigate,
  trim,
  investigate,
  partialSimonSays,
  investigate
);
​
simonSays(' Jump! ');
// Jump! 
// Jump!
// Simon Says: Jump!

复制代码

若是你正在建立纯函数,你将始终可以很是轻松地编写代码,而无需重构之前的代码来支持新的用例。

结论

复合函数要求你对编写代码的方式进行不一样层次的思考,这样将会为你带来不少好处。

由复合函数替换类层级容许你专一于,基于功能的思考去开发惟一代码,而不是基于类的思考。

隐式编程容许你经过利用柯西化和高阶函数来简化代码。

你须要构建分解后的原子函数,以便为单一责任原则和测试驱动开发建立更多可重用、可组合的代码

纯函数和局部应用函数容许经过建立功能强大、简单、可预测、可轻松测试的代码,来提高你的架构,并轻松应用到编程的最佳实践中。

相关文章
相关标签/搜索