Skip to content

24. 深入 Transducer:map 转换器是如何工作的

在上一章,我们看到了 transduce 的强大威力,它能将一系列转换操作融合成单次遍历。但我们留下了一个问题:这背后的“魔法”究竟是什么?为什么我们熟悉的 mapfilter 函数可以摇身一变,成为 Transducer 链条中的一环?

要理解这一切,我们需要揭开 Transducer 的核心定义:

一个 Transducer 是一个函数,它接受一个 Reducer,并返回一个新的、增强版的 Reducer。

换句话说,Transducer 是“Reducer 的转换器”。它是一种高阶函数,用来包装和改造 Reducer。

让我们以最基础的 map 为例,亲手构建一个 map Transducer,来彻底理解它的工作原理。

Reducer 的标准形态

首先,回忆一下 reduce 函数和 Reducer 的角色。一个标准的 Reducer 函数(例如 R.append 或我们自己写的 (acc, val) => acc + val)接受两个参数:累加器(acc)和当前值(val),并返回新的累加器。

javascript
// 一个标准的 Reducer
const sumReducer = (acc, val) => acc + val;

[1, 2, 3].reduce(sumReducer, 0); // => 6

手动实现 map Transducer

现在,我们要创建一个名为 mapping 的函数,它将扮演 map Transducer 的角色。根据定义,mapping 必须:

  1. 接受一个转换函数 transformFn (例如 x => x + 1)。
  2. 返回一个新函数,这个新函数就是 Transducer 本身。我们称之为 mapTransducer
  3. mapTransducer 必须接受一个 Reducer(例如 sumReducer),我们称之为 nextReducer
  4. mapTransducer 必须返回一个最终的、增强版的 Reducer

这个最终的 Reducer 看起来和标准的 Reducer 一样,也接受 (acc, val),但它的内部做了一件额外的事情:在调用 nextReducer 之前,先用 transformFnval 进行了转换。

让我们把这个逻辑翻译成代码:

javascript
// 这是一个高阶函数,用于创建 map transducer
const mapping = (transformFn) => {
  // 返回的是 transducer 本身
  return (nextReducer) => {
    // 返回的是最终增强版的 reducer
    return (acc, val) => {
      // 核心逻辑:在调用下一个 reducer 之前,先对 val 进行转换
      const transformedVal = transformFn(val);
      return nextReducer(acc, transformedVal);
    };
  };
};

代码解析:

  • mapping(x => x + 1):我们调用 mapping,传入一个“加 1”的转换函数。它返回了中间的 mapTransducer 函数。
  • mapTransducer(sumReducer)mapTransducer 接收了 sumReducer 作为它的 nextReducer。它返回了最内层的、最终的 Reducer。
  • 这个最终的 Reducer 现在是这样的:(acc, val) => sumReducer(acc, val + 1)

看到了吗?我们成功地将 x => x + 1 这个 map 的逻辑,“注入”到了 sumReducer 的执行流程中!

将它与 reduce 结合

现在,我们可以用原生 reduce 和我们手写的 mapping Transducer 来模拟 transduce 的效果。

javascript
const data = [1, 2, 3];

// 1. 创建一个“加一”的 map transducer
const addOneTransducer = mapping(x => x + 1);

// 2. 定义一个基础的 reducer,这里用求和为例
const sumReducer = (acc, val) => acc + val;

// 3. 用 transducer 来包装和增强基础 reducer
const enhancedReducer = addOneTransducer(sumReducer);

// 4. 将最终的 reducer 用于原生的 reduce 方法
const result = data.reduce(enhancedReducer, 0);

console.log(result); // => 9 (计算过程是 (0 + (1+1)) + (2+1)) + (3+1) = 9)

如果我们想把结果收集到数组中,只需换一个基础 Reducer 即可。

javascript
const listReducer = (acc, val) => {
  acc.push(val);
  return acc;
};

const enhancedListReducer = addOneTransducer(listReducer);

const listResult = data.reduce(enhancedListReducer, []);

console.log(listResult); // => [2, 3, 4]

这就是 Transducer 的精髓:转换逻辑(map)和收集逻辑(sumReducerlistReducer)是完全解耦的mapping Transducer 对如何收集结果一无所知,它只关心一件事:在数据流向下一个 Reducer 之前,对它进行转换。

Ramda 的实现

Ramda 内部的 R.map 在被用于 R.transduce 时,其行为就和我们手写的 mapping 函数完全一样。Ramda 通过巧妙的函数重载和内部协议,使得 R.map 既能作为普通函数处理整个数组,也能作为 Transducer 处理单个元素。

当你写 pipe(map(f), filter(g)) 时,Ramda 内部会进行类似这样的组合:

const combinedTransducer = nextReducer => map(f)(filter(g)(nextReducer));

这个组合好的 combinedTransducer 就是一个超级 Reducer 转换器,它等待着一个基础的 Reducer(如 append),然后生成一个包含了 mapfilter 双重逻辑的终极 Reducer。

总结

理解 Transducer 的关键在于牢记它的定义:一个接收 Reducer 并返回新 Reducer 的函数。

通过手动实现 mapping Transducer,我们揭示了其内部的“三层函数嵌套”结构:

  1. 最外层函数接收转换参数(transformFn),创建 Transducer。
  2. 中间层函数(Transducer 本身)接收下一个 Reducer(nextReducer),创建最终的 Reducer。
  3. 最内层函数(最终的 Reducer)接收累加器和当前值(acc, val),执行转换并调用 nextReducer

这个结构正是 Transducer 实现逻辑组合与过程解耦的核心。掌握了它,你就掌握了理解所有 Transducer(包括 filter, take 等)的钥匙。

24. 深入 Transducer:map 转换器是如何工作的 has loaded