九、函数式编程模式的元素

这是一个高级章节,重点介绍函数式编程范例和来自函数式编程世界的设计模式。现在是深入探讨为什么我们可以选择创建无状态和有状态组件的时候了。这归结为理解什么是纯函数以及不可变对象如何帮助我们预测应用程序行为。一旦我们澄清了这一点,我们将继续讨论高阶函数和高阶组件。您已经多次使用它们,但这次我们将从稍微不同的角度来看待它们。

在这本书中,我向你们挑战了许多概念,这些概念在阅读本章后会变得更加清晰。我希望您能在应用程序中接受它们,并明智地使用它们,同时牢记团队的成熟度。这些模式很好地了解,但对于 React 或 React Native development 都不是必需的。但是,在阅读 React 或 React 本机存储库的 pull 请求时,您会发现自己经常引用本章。

在本章中,我们将介绍以下主题:

  • 可变和不变结构
  • 特定函数,如纯函数
  • Maybe单子与单子模式
  • 函数式编程的好处
  • 缓存与记忆

可变和不可变对象

在我的一次编码采访中,这个概念让我感到惊讶。在我职业生涯的开始,我对可变和不变的对象知之甚少,结果事与愿违,我甚至没有意识到根本原因。

第 5 章存储模式中我解释了可变性和不变性的基础。我们甚至使用了Immutable.js图书馆。这本书的那部分重点放在商店上。现在让我们看看更大的图景。为什么我们甚至需要可变或不可变的对象?

通常,主要原因是能够快速推断应用程序的行为。例如,React 希望快速检查是否应重新渲染组件。如果您创建了一个对象A,并且您保证它永远不会更改,那么为了让自己确信没有更改,您只需要将引用与该对象进行比较。如果与之前相同,则对象A保持不变。如果对象A可以更改,我们需要比较对象A中的每个嵌套键,以确保其保持不变。如果对象A具有嵌套对象,并且我们想知道这些对象是否没有更改,那么我们需要对嵌套对象重复该过程。这是大量的工作,尤其是当对象a增长时。但我们为什么要这样做呢?

JavaScript 中的不可变原语

在 JavaScript 中,基本数据类型(数字、字符串、布尔值、未定义、null 和符号)是不可变的。对象是可变的。另外,JavaScript 是松散类型的;这意味着变量不需要是特定的类型。例如,您可以声明变量 A 并为其指定数字 5,然后决定为其指定一个对象。JavaScript 允许这样做。

为了简化事情,社区创建了两个非常重要的运动:

  • 保证对象不变性的库
  • JavaScript 的静态类型检查器,如 Flow 或 TypeScript

第一个函数提供了创建对象的函数,以保证对象的不变性。这意味着,每当您想要更改对象中的某些内容时,它将克隆自身,应用更改,并返回一个全新的不可变对象。

第二种是静态类型检查器,主要解决开发人员意外尝试分配不同于最初预期类型的值时的人为错误问题。因此,如果您将variableA声明为一个数字,则永远不能为其分配字符串。对我们来说,它意味着类型不变性。如果你想要一个不同的类型,你需要创建一个新的变量并将variableA映射到它。

An important side note on the const keyword: const operates on the reference level. It forbids a reference change. The value of a constant variable cannot be reassigned and cannot be redeclared. With primitive immutable types, it simply means freezing them for life. You can never reassign a new value to the variable. Trying to assign a different value will also fail, because primitives are immutable and it simply means creating a brand new reference. With objects that are mutable types, it simply means freezing the object reference. We cannot reassign a new object to the variable, but we can change the contents of the object. This means we can mutate what is inside. This is not very useful.

不变性成本解释

当我第一次被介绍这个概念时,我开始挠头。怎么会更快呢?如果你想修改一个对象,你需要克隆它,这是一个严重的成本与任何简单的更改。我认为这是不可接受的。我假设这是相同的成本,就好像我们在每个级别上执行平等检查一样。我是对的,也是错的。

这取决于你使用的工具。特殊的数据结构,如 Immutable.js,可以轻松地进行大量优化。但是,如果您使用spread操作符或Object.assign()克隆对象,那么您将再次重新创建整个对象,或者在不知不觉中只克隆一层深度。

"For deep cloning, we need to use other alternatives because Object.assign() copies property values. If the source value is a reference to an object, it only copies that reference value." - Mozilla JavaScript Documentation https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign. "Spread syntax effectively goes one level deep while copying an array. Therefore, it may be unsuitable for copying multidimensional arrays [...] (it's the same with Object.assign() and spread syntax)." - Mozilla JavaScript Documentation https://developer.mozilla.org/pl/docs/Web/JavaScript/Reference/Operators/Spread_syntax.

这非常方便,我们在 React 应用程序中多次滥用这一事实。让我们用一个例子来看看这个。以下是我们将对其执行操作的对象:

const someObject = {
    x: "1",
    y: 2,
    z: {
        a: 1,
        b: 2,
        c: {
            x1: 1,
            x2: 2
        }
    }
};

首先,我们将只克隆一层深度的对象,然后在克隆对象中对两层深度的对象进行变异。观察原始对象发生的情况:

function naiveSpreadClone(obj) { // objects are passed by reference
    return { ...obj };
    // copy one level deep ( nested z cloned by reference )
}
const someObject2 = naiveSpreadClone(someObject); // invoke func
someObject2.z.a = 10; // mutate two levels deep
console.log(someObject2.z.a); // logs 10
console.log(someObject.z.a); // logs 10
// nested object in original someObject mutated too!

这是突变的陷阱之一。如果您不够精通,无法理解正在发生的事情,您可能会产生难以修复的 bug。问题是,我们如何克隆两个层次的深度?见下文:

function controlledSpreadClone(obj) {
    return { ...obj, z: { ...obj.z } }; // copy 2 levels deep
}

const someObject2 = controlledSpreadClone(someObject);
someObject2.z.a = 10; // mutation only in copied object
console.log(someObject2.z.a); // logs 10
console.log(someObject.z.a); // logs 1

如果需要,可以使用此技术以这种方式复制整个对象

Copying just one level deep is often called a shallow copy.

读/写操作基准测试

为了更好地理解折衷,以及为您的特定用例选择哪个库,请查看读写操作基准。这应该作为一个总体思路。在进行最后呼叫之前,请运行您自己的测试。

我使用了ImmutableAssign 作者创建的基准测试。代码自动比较了许多库和方法,以解决 JavaScript 中的不变性。

首先,让我们看一下纯 JavaScript,它只有简单的可变结构。我们不关心任何好处,只是将其作为基准:

| 几乎全新的 MacBook Pro 15 英寸(2018) 无背景任务 | MacBook Pro 15''(2016) 运行少量后台任务 | | 可变对象和数组对象:读取(x500000):9 毫秒对象:写入(x100000):3 毫秒对象:非常深读取(x500000):31 毫秒对象:非常深写入(x100000):9 毫秒对象:合并(x100000):17 毫秒数组:读取(x500000):4 毫秒数组:写入(x100000):3 毫秒数组:深度读取(x500000):5 毫秒数组:深度写入(x100000):2 毫秒总运行时间 49 毫秒(读取)+17 毫秒(写入)+17 毫秒(合并)=83 毫秒。 | 可变对象和数组对象:读取(x500000):11 毫秒对象:写入(x100000):4 毫秒对象:非常深读取(x500000):42 毫秒对象:非常深写入(x100000):12 毫秒对象:合并(x100000):17 毫秒数组:读取(x500000):7 毫秒数组:写入(x100000):3 毫秒数组:深度读取(x500000):7 毫秒数组:深度写入(x100000):3 毫秒总运行时间 67 毫秒(读取)+22 毫秒(写入)+17 毫秒(合并)=106 毫秒。 |

在括号中,您可以看到许多已执行的操作。它是难以置信的快。没有一个不变的解决方案能够超越这个基准,因为它只使用可变的 JS 对象和数组。

根据我们阅读的深度,我们可以发现一些差异。例如,对象读取(x500000)需要 11 毫秒,而超深对象读取(x500000)需要 42 毫秒,这几乎是 4 倍长:

| 几乎全新的 MacBook Pro 15 英寸(2018) 无背景任务 | MacBook Pro 15''(2016) 运行少量后台任务 | | 不可变对象和数组(Object.assign)对象:读(x500000):13 毫秒对象:写(x100000):85 毫秒对象:非常深读(x500000):30 毫秒对象:非常深写(x100000):220 毫秒对象:合并(x100000):91 毫秒数组:读(x500000):7 毫秒数组:写(x100000):402 毫秒数组:深度读(x500000):9 毫秒数组:深度写(x100000):400 毫秒总运行时间 59 毫秒(读取)+1107 毫秒(写入)+91 毫秒(合并)=1257 毫秒。 | 不可变对象和数组(Object.assign)对象:读取(x500000):19 毫秒对象:写入(x100000):107 毫秒对象:非常深读取(x500000):33 毫秒对象:非常深写入(x100000):255 毫秒对象:合并(x100000):136 毫秒数组:读取(x500000):11 毫秒数组:写入(x100000):547 毫秒数组:深度读取(x500000):14 毫秒阵列:深度写入(x100000):504 毫秒总运行时间 77 毫秒(读取)+1413 毫秒(写入)+136 毫秒(合并)=1626 毫秒。 |

Object.assign在写操作时产生尖峰。现在我们看到了复制不需要的东西的成本。在非常深的级别上执行对象写入操作的成本几乎要高出 25 倍。数组深度写入比可变方式慢 100 到 200 倍:

| 几乎全新的 MacBook Pro 15 英寸(2018) 无背景任务 | MacBook Pro 15''(2016) 运行少量后台任务 | | Immutable.js 对象和数组对象:读(x500000):12 毫秒对象:写(x100000):19 毫秒对象:非常深读(x500000):111 毫秒对象:非常深写(x100000):80 毫秒对象:合并(x100000):716 毫秒数组:读(x500000):18 毫秒数组:写(x100000):135 毫秒数组:深度读(x500000):51 毫秒数组:深度写(x100000):97 毫秒总运行时间 192 毫秒(读取)+331 毫秒(写入)+716 毫秒(合并)=1239 毫秒。 | Immutable.js 对象和数组对象:读取(x500000):24 毫秒对象:写入(x100000):52 毫秒对象:非常深读取(x500000):178 毫秒对象:非常深写入(x100000):125 毫秒对象:合并(x100000):1207 毫秒数组:读取(x500000):24 毫秒数组:写入(x100000):255 毫秒数组:深度读取(x500000):128 毫秒数组:深度写入(x100000):137 毫秒总运行时间 354 毫秒(读取)+569 毫秒(写入)+1207 毫秒(合并)=2130 毫秒。 |

对象写入比可变方式慢 6 倍。非常深的对象写入速度比可变方式慢近 9 倍,比Object.assign()快 2.75 倍。合并操作构造作为合并作为参数传递的两个对象的结果的对象,速度要慢得多(比可变对象慢 42 倍,如果用户正在使用其他程序,甚至慢 70 倍)。

Please pay attention to the hardware used. It is either a 2016 MacBook Pro or 2018 MacBook Pro, which are both blazing-fast machines. Taking this to the mobile world will spike those benchmarks even more. The purpose of this section is to give you a general idea of how the numbers compare. Before you jump to a conclusion, please run your own tests on a specific hardware relevant to your project.

纯函数

在本节中,我们将回到我们已经学习过的纯函数,但现在从不同的角度来看。你还记得 Redux 试图尽可能的明确吗?这是有原因的。任何隐含的东西通常都是麻烦的根源。你还记得数学课上的函数吗?这些都是 100%明确的。除了将输入转换为输出之外,没有其他事情发生。

然而,在 JavaScript 中,函数可以有隐式输出。它可能会改变一个值,改变一个外部系统,许多其他事情可能会发生在功能范围之外。您已经在第五章商店模式中了解到了这一点。所有这些隐式输出通常被称为副作用。

我们需要解决所有不同口味的副作用。我们的武器之一是不变性,它保护我们不受隐含的外部对象变化的影响。这就是不变性,因为它保证不会发生这样的事情。

在 JavaScript 中,我们不能通过引入不变性等武器来消除所有副作用。有些需要语言级别的工具,但这些工具不可用。在 Haskell 等函数式编程语言中,甚至输入/输出都由一个称为IO()的独立结构控制。然而,在 JavaScript 中,我们需要自己处理它。这意味着我们无法避免某些函数不纯净,因为它们需要处理 API 调用。

另一个例子是随机性。任何使用Math.random的函数都不能被视为纯函数,因为此类函数的某些部分依赖于随机数生成器,这违背了纯函数的目的。一旦使用某些参数调用函数,就不能保证收到相同的输出。

同样,一切依赖时间的东西都是不纯洁的。如果函数的执行依赖于月、日、秒甚至年,则不能将其视为纯函数。在某些情况下,同一个参数不会给出相同的输出。

最后,这一切都归结于执行链。如果你想说操作的子集是纯的,那么你需要知道它们中的每一个都是纯的。最简示例是使用另一个函数的函数:

const example = someArray => someFunc => someFunc(someArray);

在这个例子中,我们不知道someFunc将是什么。如果someFunc不纯,example函数也不纯。

Redux 中的纯函数

好消息是,我们可以将副作用推送到应用程序的某个位置,并在真正需要时在循环中调用它们。这就是 Flux 所做的。Redux 更进一步地支持它,只允许纯函数作为减缩器。这是可以理解的。当不纯净的部分已经完成时,称为还原剂。从那个以后,我们可以保持不变性,至少在 Redux 存储方面是如此。

Some may question whether this is a good choice in terms of performance. Trust me, it is. We have a really low number of events happening (that need to be reduced, and hence affect the store) in comparison with state accesses and selectors operating on the computed state.

作为保持国家不变的回报,我们得到了巨大的好处。我们可以知道导致这种特殊状态的函数应用程序的顺序。如果我们真的需要,我们可以跟踪它。这是巨大的。我们可以在测试环境中再次应用这些函数,并保证输出完全相同。这是由于功能是纯的,因此不会产生副作用。

缓存纯函数

缓存是一种记忆计算的技术。如果您保证对于某些参数,您的函数将始终返回相同的值,那么您可以安全地计算它一次,并始终返回这些特定参数的计算值。

让我们看看通常出于教学目的而提出的琐碎实现:

const memoize = yourFunction => {
  const cache = {};

  return (...args) => {
    const cacheKey = JSON.stringify(args);
    if (!cache[cacheKey]) {
        cache[cacheKey] = yourFunction(...args);
    }
    return cache[cacheKey];
  };
};

这是一项功能强大的技术,用于重新选择库。

参考透明度

纯函数是引用透明,这意味着它们的函数调用可以替换为给定参数的相应结果。

现在,看看引用透明和引用不透明函数的示例:

 let globalValue = 0;

 const inc1 = (num) => { // Referentially opaque (has side effects)
   globalValue += 1;
   return num + globalValue;
 }

 const inc2 = (num) => { // Referentially transparent
   return num + 1;
 }

让我们想象一个数学表达式:

inc(4) + inc(4) * 5

// With referentially transparent function you can simplify to:
inc(4) * ( 1 + 1*5 )
// and even to
inc(4) * 6

请注意,如果函数不是引用透明的,则需要避免此类简化。前面或x() + x() * 0之类的表达方式都是诱人的陷阱。

你是否利用它取决于你自己。另外,请参见本章末尾的进一步阅读部分。

除了单子什么都有

monad 这个词多年来一直臭名昭著。不是因为它是一个非常有用的构造,而是因为它引入了复杂性。还有一种普遍的看法是,一旦你理解了单子,你就失去了解释它们的能力。

"In order to understand monads, you need to first learn Haskell and Category Theory. I think this is like saying: In order to understand burritos, you must first learn Spanish."

- Douglas Crockford: Monads and Gonads (YUIConf Evening Keynote) https://www.youtube.com/watch?v=dkZFtimgAcM.

单子是一种在特殊情况下组合函数的方法,例如可为空的值、副作用、计算或只是条件执行。单子的这种定义使它成为上下文持有者。这就是为什么 X 的单子不等同于 X。这个 X 在被视为monad<X>之前,这个 X 某物需要首先被提升,这仅仅意味着创建所需的上下文。如果我们不再需要 monad,我们可以将结构扁平化为 X,这相当于失去上下文。

这就像拆开圣诞礼物一样。你很确定里面有礼物,但这取决于你一整年是否都很好。在一些罕见的不当行为案例中,你可能最终会被棍子或煤块压在里面。这就是Maybe<X>单子的工作原理。它可能是 X 或者什么都没有。它可以很好地处理可为空的 API 值。

也许打电话给我

在我们的代码中有一个地方要求简化。请看taskSelector

export const tasksSelector = state => state.tasks;

export const tasksEntitiesSelector = createSelector(
    tasksSelector,
    tasks => (tasks ? tasks.get('entities') : null)
);

export const getTaskById = taskId => createSelector(
    tasksEntitiesSelector,
    entities => (entities
        ? entities.find(task => task.id === taskId)
        : null)
); 

我们总是担心我们是否收到了一些东西。这是将此类工作委托给Maybe单子的完美案例。一旦我们实施Maybe,以下代码将完全发挥作用:

import Maybe from '../../../../utils/Maybe';

export const tasksSelector = state => Maybe(state).map(x => x.tasks);

export const tasksEntitiesSelector = createSelector(
    tasksSelector,
    maybeTasks => maybeTasks.map(tasks => tasks.get('entities'))
);

export const getTaskById = taskId => createSelector(
    tasksEntitiesSelector,
    entities => entities.map(e => e.find(task => task.id === taskId))
);

好的,到目前为止,您对我们需要实现的Maybe单子有一点了解:当null/undefinedSomethingnullundefined时,它需要为空:

const Maybe = (value) => {
    const Nothing = {
        // Some trivial implementation
    };
    const Something = val => ({
        // Some trivial implementation
    });

    return (typeof value === 'undefined' || value === null)
        ? Nothing
        : Something(value);
};

到目前为止,非常容易。问题是,我们既没有实现Nothing也没有实现Something。别担心,这很简单,就像我的评论一样。

我们需要它们对三个功能作出反应:

  • isNothing
  • val
  • map

前两个函数很简单:

  • isNothingNothing返回trueSomething返回false
  • valNothing返回nullSomething返回其值

最后一个是map,对于Nothing应该什么都不做(返回本身),对于Something应该对值应用函数:

Applying toUpperCase on an ordinary string type and on the Maybe monad using the map function

让我们实现这个逻辑:

// src / Chapter 9 / Example 1 / src / utils / Maybe.js
const Maybe = (value) => {
    const Nothing = {
        map: () => this,
        isNothing: () => true,
        val: () => null
    };
    const Something = val => ({
        map: fn => Maybe(fn.call(this, val)),
        isNothing: () => false,
        val: () => val
    });

    return (typeof value === 'undefined' || value === null)
        ? Nothing
        : Something(value);
};

export default Maybe;

我们来了,不到 20 行。我们的选择器现在使用的是Maybe单子。我们需要做的最后一件事是确定最终用途;它应该在选择器调用后请求值,如以下示例所示:

// src / Chapter 9 / Example 1
//         src/features/tasks/containers/TaskDetailsContainer.js

const mapStateToProps = (state, ownProps) => ({
    task: getTaskById(ownProps.taskId)(state).val()
});

我们的Maybe实现是一种很酷的模式,可以避免空检查负担,但它真的是单子吗?

Monad 接口要求

更正式地说,monad 接口应该定义两个基本运算符:

  • Return(a -> M a),一种接受a类型并将其包装成 monad(M a的操作
  • Bind(M a -> (a -> M b) -> M b),一个接受两个参数的操作:一个是 a 类型的 monad,另一个是对a进行操作并返回M ba -> M bmonad)的函数

在这些术语中,我们的构造函数是return函数。但是,我们的 map 功能不符合bind要求。它需要一个函数将a转换为ba -> b,然后我们的map函数自动将b包装为M b

除此之外,我们的单子需要遵守三条单子定律:

  • 左身份:
// for all x, fn
Maybe(x).map(fn) == Maybe(fn(x))
  • 权利身份:
// for all x
Maybe(x).map(x => x) == Maybe(x)
  • 结合性:
// for all x, fn, gn
Maybe(x).map(fn).map(gn) == Maybe(x).map(x => gn(fn(x)));

数学证明超出了本书的范围。然而,我们可以玩弄这些定律,看看它们是否适用于一些随机的例子:

// Left identity example
Maybe("randomtext")
.map(str => String.prototype.toUpperCase.call(str))
.val() // RANDOMTEXT

Maybe(String.prototype.toUpperCase.call("randomtext"))
.val()) // RANDOMTEXT

// Right identity example
Maybe("randomtext").map(str => str).val() // randomtext
Maybe("randomtext").val() // randomtext

// Associativity
const f = str => str.replace('1', 'one');
const g = str => str.slice(1);

Maybe("1 2 3").map(f).map(g).val() // ne 2 3
Maybe("1 2 3").map(str => g(f(str))).val() // ne 2 3

高阶函数

我们已经了解了高阶组件,在本节中,我们将了解更一般的概念,称为高阶函数。

看看这个例子。这很简单。你甚至不会注意到你创造了什么特别的东西:

const add5 = x => x + 5; // function
const applyTwice = (f, x) => f(f(x)); // higher order function

applyTwice(add5, 7); // 17

那么什么是高阶函数呢?

高阶函数是执行以下操作之一的函数:

  • 将一个或多个函数作为参数
  • 返回一个函数

就这样,;很简单。

高阶函数的例子

有许多函数是高阶函数,您每天都会使用它们:

  • Array.prototype.map
someArray.map(function callback(currentValue, index, array){
    // Return new element
});

// or in the shorter form
someArray.map((currentValue, index, array) => { //... });
  • Array.prototype.filter
someArray.filter(function callback(currentValue, index, array){
    // Returns true or false
});

// or in the shorter form
someArray.filter((currentValue, index, array) => { //... });
  • Array.prototype.reduce
someArray.reduce(
    function callback(previousValue, currentValue, index, array){
        // Returns whatever
    },
    initialValue
);

// or in the shorter form
someArray.reduce((previousValue, currentValue, index, array) => {
    // ... 
}, initialValue);

// previousValue is usually referred as accumulator or short acc
// reduce callback is also referred as fold function

当然还有composecallcurry等功能,我们已经了解了这些功能。

通常,任何接受回调的函数都是高阶函数。在任何地方都可以使用这样的函数。

你还记得那些曲子写得有多好吗?见下文:

someArray
    .map(...)
    .filter(...)
    .map(...)
    .reduce(...)

但其中一些没有,比如回调。你听说过地狱吗?

回调中的回调中的回调,这是一个回调地狱。这就是承诺被发明的原因。

然后,突然间,Promise地狱开始了,所以聪明的人为承诺创造了一种语法糖:asyncawait

撇开函数语言不谈

首先,请阅读 David 的有趣观点。

“等等,等等,等等。持久数据结构的性能与 JavaScript MVC 的未来有什么关系?

很多。

我们将看到,不可变数据(可能是非直观的)如何允许一个新的库 Om 在没有用户手动优化的情况下,超越性能合理的 JavaScript MVC(如主干.js)。Om 本身是建立在 Facebook 上绝对精彩的 React 库之上的。”

-JavaScript MVC 框架的未来 David Nolen(swannodette),2013 年 12 月 17 日 http://swannodette.github.io/2013/12/17/the-future-of-javascript-mvcs

在撰写本文时(2018 年 9 月),主干网已经停业。即使是角斗士的流行,也要与之竞争。React 以惊人的速度占领了市场,一旦它最终将其许可证改为麻省理工学院,它甚至加速了发展。

有趣的是,requestAnimationFramerAF)并不像人们曾经认为的那么重要。

"We do batching between different setState()s within one event handler (everything is flushed when you exit it). For many cases this works well enough and doesn’t have pitfalls of using rAF for every update.

We are also looking at asynchronous rendering by default. But rAF doesn’t really help much if the rendered tree is large. Instead we want to split non-critical updates in chunks using rIC until they’re ready to be flushed.

(...) We use a concept of "expiration". Updates coming from interactive events get very short expiration time (must flush soon), network events get more time (can wait). Based on that we decide what to flush and what to time-slice."

我想让你们从这两段引语中学到的教训是:不要想当然,不要夸耀一种方法胜过另一种方法,要学会在什么情况下一种方法优于另一种方法。函数式编程与之类似;像我曾经想的那样,放弃这一章是愚蠢的。我有这样的感觉:这与本地程序员的反应有关吗?是的。如果它很受欢迎,足以淹没社区中的许多公共 PRs,你肯定会接触到这些概念,我希望你做好准备。

术语

不要被函子、内函子、余子和余程所吓倒,它们从理论抽象中获取有用的东西。让理论专家来照顾他们。数学怪才们总是走在前面,通常这是件好事,但不要太疯狂。公事公办。期限不能等你证明范畴论中最伟大的定律。

专注于理解眼前的好处,如本书中概述的好处。如果您发现自己所在的团队反对函数式编程模式,请不要强制执行它们。毕竟,它在 JavaScript 中不如在 Haskell 中重要。

"Using fancy words instead of simple, common ones makes things harder to understand. Your writing will be clearer if you stick with a small vocabulary."

建筑抽象

在本章的开头,我们对不可变库进行了基准测试,并比较了它们的性能。与所有事情一样,我强烈建议您在投入任何库、模式或做事方式之前花一些时间。

大多数采用函数式编程模式的库这样做是为了真正的好处。如果你不确定,把它留给其他人,并坚持你众所周知的命令式模式。事实证明,简单的代码通常在引擎级别得到更好的优化

React 并不痴迷于纯函数

当你第一次进入 React 生态系统时,你可能会得到一点惊喜。有很多例子使用纯函数,讨论时间旅行、使用 Redux 以及一个商店来管理它们。

事实是,React 和 Redux 都不使用纯函数。事实上,两个库中都有很多函数在外部范围内执行突变:

// Redux library code
// redux/src/createStore.js

let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false

// Check yourself:
https://github.com/reduxjs/redux/blob/1448a7c565801029b67a84848582c6e61822f572/src/createStore.js  

这些变量正在被其他函数修改。

现在,看看如何记住图书馆的警告:

let didWarnAboutMaps = false;

// (...)

if (__DEV__) {
  if (iteratorFn === children.entries) {
    warning(
      didWarnAboutMaps,
      'Using Maps as children is unsupported (...)'
    );
    didWarnAboutMaps = true;
  }
}

// Check yourself
https://github.com/facebook/react/blob/f9358c51c8de93abe3cdd0f4720b489befad8c48/packages/react/src/ReactChildren.js

这种微小的变异取决于环境。

If you maintain a library with such checks, current build tools, such as webpack, can remove this dead code when building a production-minified file. By dead code, I mean code paths (like the preceding if statement) that will never be accessed because of the environment (production).

当谈到 Facebook 时,他们并不羞于表明他们的代码库在某些地方很棘手:

Facebook codebase screenshot, posted by Dan Abramov on Twitter 

总结

在本章中,我们深入探讨了 JavaScript 编程最深奥的分支之一。我们学习了单子,学会了如何利用它们来获得更大的利益,学会了如果我们真的不需要的话,如何不去关心数学定律。然后,我们可以自如地使用诸如纯函数、可变/不可变对象和引用透明性之类的词汇表。

我们知道,如果需要,纯函数有一种缓存模式。这种很棒的方法在很多 Flux 应用程序中都很有用。现在,您可以有效地使用选择器,并使用 Maybe monad 使它们非常简单,从而消除了空检查负担。

有了所有这些专业知识,现在是时候学习维护依赖关系和大型代码库的挑战了。在下一章中,您将面临每一个大型代码库的重大挑战,相信我,每一个大型公司都会在某个时刻与此进行斗争,无论他们使用多少编程模式或依赖多少库。

进一步阅读

  • 一本关于函数式编程的最合适的指南——一本关于 JavaScript 函数式编程的免费书籍:

https://github.com/MostlyAdequate/mostly-adequate-guide

  • 您可能希望与重选库一起使用的缓存函数示例:

https://github.com/reduxjs/reselect#q--default-memoization-function-is-no-good-can-i-use-a-different-one

  • 关于参考透明度的信息:

https://softwareengineering.stackexchange.com/questions/254304/what-is-referential-transparency

  • Eric 的 Elliott 精通 JavaScript 访谈系列插曲,纯功能:

https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976

  • 一篇预测未来的历史帖子,JavaScript MVC 的未来

http://swannodette.github.io/2013/12/17/the-future-of-javascript-mvcs

  • 这很古老,但仍然值得一读,反应性的一般理论

https://github.com/kriskowal/gtor

  • 以下关于 JavaScript 中 FP 的书籍,JavaScript Allonge(在线免费阅读):

https://leanpub.com/javascriptallongesix/read#leanpub-自动关于 javascript 分配

  • Monad laws(Haskell Wiki):

https://wiki.haskell.org/Monad_laws

  • 道格拉斯·克罗克福德、单子和性腺:

https://www.youtube.com/watch?v=dkZFtimgAcM

  • Immutable.js 如何使用 Trie 图优化写入操作:

https://medium.com/@dtinth/immutable-js-persistent-data-structures-and-structural-sharing-6d163fbd73d2

https://en.wikipedia.org/wiki/Trie

  • 默认情况下应使用requestAnimationFrame

https://github.com/facebook/react/issues/11171

  • GitHub 上一个很棒的函数式编程集合:

https://github.com/xgrommx/awesome-functional-programming/blob/master/README.md

  • 如果你爱上了函数式编程,这里有一个非常好的资源, 学习 Haskell 非常好(需要了解 Haskell):

http://learnyouahaskell.com/chapters