我对传感器的理解是否正确?
Is my understanding of transducers correct?
让我们从一个定义开始:transducer
是一个接受 reducer
函数和 returns 一个 reducer
函数的函数。
A reducer
是一个二元函数,它接受一个累加器和一个值,returns 接受一个累加器。 reducer 可以使用 reduce
函数执行(注意:所有函数都是柯里化的,但为了可读性,我已经列出了这个以及 pipe
和 compose
的定义 - 你可以在 live demo):
中看到它们
const reduce = (reducer, init, data) => {
let result = init;
for (const item of data) {
result = reducer(result, item);
}
return result;
}
使用reduce
我们可以实现map
和filter
功能:
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);
正如我们所看到的,map
和 filter
之间有一些相似之处,并且这两个函数都只适用于数组。另一个缺点是,当我们组合这两个函数时,在每个步骤中都会创建一个临时数组,该数组将传递给另一个函数。
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 帮助我们解决了这个问题:当我们使用 transducers 时,不会创建临时数组,我们可以概括我们的函数,使其不仅适用于数组。 转换器需要transduce
函数才能工作转换器通常通过传递给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
定义数组 map
和 filter
,因为它是如此可组合:
const map = (xf, data) => transduce(mapping(xf), arrReducer, [], data);
const filter = (predicate, data) => transduce(filtering(predicate), arrReducer, [], data);
live 版本如果你想 运行 代码 -> https://runkit.com/marzelin/transducers
我的推理有道理吗?
您的理解是正确的,但不完整。
除了您描述的概念之外,转换器还可以执行以下操作:
- 支持提前退出语义
- 支持完成语义
- 有状态
- 支持阶跃函数的初始值。
例如,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
减少操作提早退出。
它需要能够(可选地)调用不带初始值参数的步进函数:
// handles lack of initial value
let mapDouble = map(n => n * 2);
console.assert(reduce(mapDouble(sum), [1,2]) === 6);
此处调用 sum
不带参数 returns 添加标识(零)以进行还原。
为了完成这个,这里有一个辅助函数:
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);
}
};
这需要一个初始值(或一个可以提供初始值的函数)和一个用于播种的减速器:
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 的演讲尽可能准确地实现语义。
让我们从一个定义开始:transducer
是一个接受 reducer
函数和 returns 一个 reducer
函数的函数。
A reducer
是一个二元函数,它接受一个累加器和一个值,returns 接受一个累加器。 reducer 可以使用 reduce
函数执行(注意:所有函数都是柯里化的,但为了可读性,我已经列出了这个以及 pipe
和 compose
的定义 - 你可以在 live demo):
const reduce = (reducer, init, data) => {
let result = init;
for (const item of data) {
result = reducer(result, item);
}
return result;
}
使用reduce
我们可以实现map
和filter
功能:
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);
正如我们所看到的,map
和 filter
之间有一些相似之处,并且这两个函数都只适用于数组。另一个缺点是,当我们组合这两个函数时,在每个步骤中都会创建一个临时数组,该数组将传递给另一个函数。
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 帮助我们解决了这个问题:当我们使用 transducers 时,不会创建临时数组,我们可以概括我们的函数,使其不仅适用于数组。 转换器需要转换器通常通过传递给transduce
函数才能工作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
定义数组 map
和 filter
,因为它是如此可组合:
const map = (xf, data) => transduce(mapping(xf), arrReducer, [], data);
const filter = (predicate, data) => transduce(filtering(predicate), arrReducer, [], data);
live 版本如果你想 运行 代码 -> https://runkit.com/marzelin/transducers
我的推理有道理吗?
您的理解是正确的,但不完整。
除了您描述的概念之外,转换器还可以执行以下操作:
- 支持提前退出语义
- 支持完成语义
- 有状态
- 支持阶跃函数的初始值。
例如,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
减少操作提早退出。
它需要能够(可选地)调用不带初始值参数的步进函数:
// handles lack of initial value
let mapDouble = map(n => n * 2);
console.assert(reduce(mapDouble(sum), [1,2]) === 6);
此处调用 sum
不带参数 returns 添加标识(零)以进行还原。
为了完成这个,这里有一个辅助函数:
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);
}
};
这需要一个初始值(或一个可以提供初始值的函数)和一个用于播种的减速器:
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 的演讲尽可能准确地实现语义。