【问题标题】:Is my understanding of transducers correct?我对换能器的理解正确吗?
【发布时间】:2018-09-11 10:59:04
【问题描述】:

让我们从定义开始:transducer 是一个接受 reducer 函数并返回 reducer 函数的函数。

reducer 是一个二进制函数,它接受一个累加器和一个值并返回一个累加器。可以使用 reduce 函数执行 reducer(注意:所有函数都是 curried 但我已经列出了这个以及 pipecompose 的定义,以便于阅读 - 你可以在 @987654321 中看到它们@):

const reduce = (reducer, init, data) => {
  let result = init;
  for (const item of data) {
    result = reducer(result, item);
  }
  return result;
}

使用reduce,我们可以实现mapfilter 函数:

const mapReducer = xf => (acc, item) => [...acc, xf(item)];
const map = (xf, arr) => reduce(mapReducer(xf), [], arr);

const filterReducer = predicate => (acc, item) => predicate(item) ?
  [...acc, item] :
  acc;
const filter = (predicate, arr) => reduce(filterReducer(predicate), [], arr);

我们可以看到mapfilter 之间有一些相似之处,并且这两个函数都只适用于数组。另一个缺点是,当我们组合这两个函数时,每一步都会创建一个临时数组,该数组会被传递给另一个函数。

const even = n => n % 2 === 0;
const double = n => n * 2;

const doubleEven = pipe(filter(even), map(double));

doubleEven([1,2,3,4,5]);
// first we get [2, 4] from filter
// then final result: [4, 8]

换能器帮助我们解决了这个问题:当我们使用换能器时,不会创建临时数组,我们可以泛化我们的函数,使其不仅适用于数组。 Transducers 需要transduce 函数才能工作 Transducers 通常通过传递给transduce 函数来执行:

const transduce = (xform, iterator, init, data) =>
  reduce(xform(iterator), init, data);

const mapping = (xf, reducer) => (acc, item) => reducer(acc, xf(item));

const filtering = (predicate, reducer) => (acc, item) => predicate(item) ?
  reducer(acc, item) :
  acc;

const arrReducer = (acc, item) => [...acc, item];

const transformer = compose(filtering(even), mapping(double));

const performantDoubleEven = transduce(transformer, arrReducer, [])

performantDoubleEven([1, 2, 3, 4, 5]); // -> [4, 8] with no temporary arrays created

我们甚至可以使用transducer 定义数组mapfilter,因为它是如此可组合:

const map = (xf, data) => transduce(mapping(xf), arrReducer, [], data);

const filter = (predicate, data) => transduce(filtering(predicate), arrReducer, [], data);

如果您想运行代码,可以使用实时版本 -> https://runkit.com/marzelin/transducers

我的推理有道理吗?

【问题讨论】:

  • 我没有看你的代码,但是描述是正确的,输出看起来是正确的。请记住,有两种方法可以对列表执行一系列转换:您可以将第一个转换应用于每个元素,然后应用第二个等。或者您可以组合转换,将组合转换应用于第一个元素,然后第二,等等。传感器是第二件事。它们甚至比这更酷,它们没有说明集合的性质,所以你可以在 Observables、生成器等上使用它们。
  • 我在 JavaScript 中 implemented transducers 玩了一会儿,如果你愿意,可以查看 repo 并比较笔记。
  • 这很好。在此之前不知道传感器是一回事。
  • @rmn 它们是为 Clojure 语言开发的,但您几乎可以用任何具有高阶函数的语言来实现它们。见this for details
  • 是的,您的理解似乎很好,符合普遍的期望。你从哪里学来的?请注意,对于 js,还有一个 interoperability protocol

标签: javascript functional-programming transducer


【解决方案1】:

你的理解是正确的,但不完整。

除了您描述的概念之外,转换器还可以执行以下操作:

  • 支持提前退出语义
  • 支持完成语义
  • 有状态
  • 支持步进函数的初始值。

例如,JavaScript 中的实现需要这样做:

// Ensure reduce preserves early termination
let called = 0;
let updatesCalled = map(a => { called += 1; return a; });
let hasTwo = reduce(compose(take(2), updatesCalled)(append), [1,2,3]).toString();
console.assert(hasTwo === '1,2', hasTwo);
console.assert(called === 2, called);

这里因为调用了take,所以减少操作提前退出。

它需要能够(可选地)调用没有参数的 step 函数作为初始值:

// handles lack of initial value
let mapDouble = map(n => n * 2);
console.assert(reduce(mapDouble(sum), [1,2]) === 6);

这里调用sum 不带参数返回加法标识(零)以减少种子。

为了完成这个,这里有一个辅助函数:

const addArities = (defaultValue, reducer) => (...args) => {
  switch (args.length) {
    case 0: return typeof defaultValue === 'function' ? defaultValue() : defaultValue;
    case 1: return args[0];
    default: return reducer(...args);
  }
};

这需要一个初始值(或可以提供初始值的函数)和一个 reducer 来播种:

const sum = addArities(0, (a, b) => a + b);

现在sum 具有正确的语义,这也是第一个示例中append 的定义方式。对于有状态的传感器,请查看 take(包括辅助函数):

// Denotes early completion
class _Wrapped {
  constructor (val) { this[DONE] = val }
};

const isReduced = a => a instanceof _Wrapped;
// ensures reduced for bubbling
const reduced = a => a instanceof _Wrapped ? a : new _Wrapped(a);
const unWrap = a => isReduced(a) ? a[DONE] : a;

const enforceArgumentContract = f => (xform, reducer, accum, input, state) => {
  // initialization
  if (!exists(input)) return reducer();
  // Early termination, bubble
  if (isReduced(accum)) return accum;
  return f(xform, reducer, accum, input, state);
};

/*
 * factory
 *
 * Helper for creating transducers.
 *
 * Takes a step process, intial state and returns a function that takes a
 * transforming function which returns a transducer takes a reducing function,
 * optional collection, optional initial value. If collection is not passed
 * returns a modified reducing function, otherwise reduces the collection.
 */
const factory = (process, initState) => xform => (reducer, coll, initValue) => {
  let state = {};
  state.value = typeof initState === 'function' ? initState() : initState;
  let step = enforceArgumentContract(process);
  let trans = (accum, input) => step(xform, reducer, accum, input, state);
  if (coll === undefined) {
    return trans; // return transducer
  } else if (typeof coll[Symbol.iterator] === 'function') {
    return unWrap(reduce(...[trans, coll, initValue].filter(exists))); 
  } else {
    throw NON_ITER;
  }
};

const take = factory((n, reducer, accum, input, state) => {
  if (state.value >= n) {
    return reduced(accum);
  } else {
    state.value += 1;
  }
  return reducer(accum, input);
}, () => 0);

如果您想查看所有这些操作,我不久前发了一个little library。尽管我忽略了 Cognitect 的互操作协议(我只是想了解概念),但我确实尝试根据 Rich Hickey 在 Strange Loop 和 Conj 的演讲尽可能准确地实现语义。

【讨论】:

  • 老实说,这些大多是 hack,不是很干净。尤其是提供初始化值并向其添加状态很容易搞砸(IIRC 认知协议在那里有一个基本问题,不确定您的实现有多少偏差)。我不建议根据参数长度来区分功能 - 像这样的重载方法在 clojure 中似乎很好,但在 JavaScript(尤其是函数式 JavaScript)中却不是。
  • @Bergi 我不打算让它成为生产就绪的(我什至没有添加测试框架,只是使用了console.assert)。我只是为了好玩而天真地实现它,边看演讲边抄录。如果我想实际使用传感器,我会使用例如拉姆达。我也同意 arity 重载不是非常 JavaScript-y,但同样是天真的转录。 OP 询问他是否理解传感器背后的概念,我试图概述一些被遗漏的概念。
猜你喜欢
  • 2012-01-27
  • 2021-09-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-24
  • 2021-02-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多