如何以正确的顺序链接映射和过滤函数
Posted
技术标签:
【中文标题】如何以正确的顺序链接映射和过滤函数【英文标题】:How to chain map and filter functions in the correct order 【发布时间】:2017-05-26 10:06:13 【问题描述】:我真的很喜欢链接 Array.prototype.map
、filter
和 reduce
来定义数据转换。不幸的是,在最近的一个涉及大型日志文件的项目中,我无法再多次循环遍历我的数据......
我的目标:
我想创建一个链接.filter
和.map
方法的函数,而不是立即映射到数组,而是组成一个循环遍历数据的函数一次。即:
const DataTransformation = () => (
map: fn => (/* ... */),
filter: fn => (/* ... */),
run: arr => (/* ... */)
);
const someTransformation = DataTransformation()
.map(x => x + 1)
.filter(x => x > 3)
.map(x => x / 2);
// returns [ 2, 2.5 ] without creating [ 2, 3, 4, 5] and [4, 5] in between
const myData = someTransformation.run([ 1, 2, 3, 4]);
我的尝试:
受this answer 和this blogpost 的启发,我开始编写Transduce
函数。
const filterer = pred => reducer => (acc, x) =>
pred(x) ? reducer(acc, x) : acc;
const mapper = map => reducer => (acc, x) =>
reducer(acc, map(x));
const Transduce = (reducer = (acc, x) => (acc.push(x), acc)) => (
map: map => Transduce(mapper(map)(reducer)),
filter: pred => Transduce(filterer(pred)(reducer)),
run: arr => arr.reduce(reducer, [])
);
问题:
上面Transduce
sn-p 的问题在于它“向后”运行...我链接的最后一个方法是第一个被执行的:
const someTransformation = Transduce()
.map(x => x + 1)
.filter(x => x > 3)
.map(x => x / 2);
// Instead of [ 2, 2.5 ] this returns []
// starts with (x / 2) -> [0.5, 1, 1.5, 2]
// then filters (x < 3) -> []
const myData = someTransformation.run([ 1, 2, 3, 4]);
或者,更抽象地说:
从:
Transducer(concat).map(f).map(g) == (acc, x) => concat(acc, f(g(x)))
收件人:
Transducer(concat).map(f).map(g) == (acc, x) => concat(acc, g(f(x)))
类似于:
mapper(f) (mapper(g) (concat))
我想我理解为什么会发生这种情况,但如果不更改函数的“接口”,我无法弄清楚如何解决它。
问题:
我怎样才能使我的Transduce
方法链filter
和map
以正确的顺序操作?
注意事项:
我只是在了解我正在尝试做的一些事情的命名。如果我错误地使用了Transduce
术语,或者是否有更好的方法来描述问题,请告诉我。
我知道我可以使用嵌套的 for
循环来做同样的事情:
const push = (acc, x) => (acc.push(x), acc);
const ActionChain = (actions = []) =>
const run = arr =>
arr.reduce((acc, x) =>
for (let i = 0, action; i < actions.length; i += 1)
action = actions[i];
if (action.type === "FILTER")
if (action.fn(x))
continue;
return acc;
else if (action.type === "MAP")
x = action.fn(x);
acc.push(x);
return acc;
, []);
const addAction = type => fn =>
ActionChain(push(actions, type, fn ));
return
map: addAction("MAP"),
filter: addAction("FILTER"),
run
;
;
// Compare to regular chain to check if
// there's a performance gain
// Admittedly, in this example, it's quite small...
const naiveApproach =
run: arr =>
arr
.map(x => x + 3)
.filter(x => x % 3 === 0)
.map(x => x / 3)
.filter(x => x < 40)
;
const actionChain = ActionChain()
.map(x => x + 3)
.filter(x => x % 3 === 0)
.map(x => x / 3)
.filter(x => x < 40)
const testData = Array.from(Array(100000), (x, i) => i);
console.time("naive");
const result1 = naiveApproach.run(testData);
console.timeEnd("naive");
console.time("chain");
const result2 = actionChain.run(testData);
console.timeEnd("chain");
console.log("equal:", JSON.stringify(result1) === JSON.stringify(result2));
这是我在堆栈 sn-p 中的尝试:
const filterer = pred => reducer => (acc, x) =>
pred(x) ? reducer(acc, x) : acc;
const mapper = map => reducer => (acc, x) => reducer(acc, map(x));
const Transduce = (reducer = (acc, x) => (acc.push(x), acc)) => (
map: map => Transduce(mapper(map)(reducer)),
filter: pred => Transduce(filterer(pred)(reducer)),
run: arr => arr.reduce(reducer, [])
);
const sameDataTransformation = Transduce()
.map(x => x + 5)
.filter(x => x % 2 === 0)
.map(x => x / 2)
.filter(x => x < 4);
// It's backwards:
// [-1, 0, 1, 2, 3]
// [-0.5, 0, 0.5, 1, 1.5]
// [0]
// [5]
console.log(sameDataTransformation.run([-1, 0, 1, 2, 3, 4, 5]));
【问题讨论】:
map: map => Transduce(mapper(map)(reducer)),意思是:返回一个先映射的函数,再做原始的reducer。 传感器构建堆栈。试试run: arr => arr.reduceRight(reducer, [])
@ftor 我想让run
中的reducer
成为之前通过的所有map
和filter
方法的组合。我认为问题不在于我用数据减少arr
的顺序,而是我未能正确返回新的reducer
方法的方式。例如:mapper(f) (mapper(g) (concat));
返回类似于 (acc, x) => concat(acc, g(f(x)))
的减速器,而我的 Transduce(concat).map(f).map(g)
返回 (acc, x) => concat(acc, f(g(x)))
【参考方案1】:
在我们更清楚之前
我真的很喜欢链式...
我明白了,我会安抚你,但你会明白强制你的程序通过链式 API 是不自然的,而且在大多数情况下,麻烦多于其价值。
const Transduce = (reducer = (acc, x) => (acc.push(x), acc)) => ( map: map => Transduce(mapper(map)(reducer)), filter: pred => Transduce(filterer(pred)(reducer)), run: arr => arr.reduce(reducer, []) );
我想我理解它为什么会发生,但我不知道如何在不改变我的函数的“接口”的情况下解决它。
问题确实出在您的Transduce
构造函数上。您的 map
和 filter
方法将 map
和 pred
堆叠在换能器链的外部,而不是将它们嵌套在内部。
下面,我已经实现了您的Transduce
API,它以正确的顺序评估地图和过滤器。我还添加了一个 log
方法,以便我们可以看到 Transduce
的行为方式
const Transduce = (f = k => k) => (
map: g =>
Transduce(k =>
f ((acc, x) => k(acc, g(x)))),
filter: g =>
Transduce(k =>
f ((acc, x) => g(x) ? k(acc, x) : acc)),
log: s =>
Transduce(k =>
f ((acc, x) => (console.log(s, x), k(acc, x)))),
run: xs =>
xs.reduce(f((acc, x) => acc.concat(x)), [])
)
const foo = nums =>
return Transduce()
.log('greater than 2?')
.filter(x => x > 2)
.log('\tsquare:')
.map(x => x * x)
.log('\t\tless than 30?')
.filter(x => x < 30)
.log('\t\t\tpass')
.run(nums)
// keep square(n), forall n of nums
// where n > 2
// where square(n) < 30
console.log(foo([1,2,3,4,5,6,7]))
// => [ 9, 16, 25 ]
未开发的潜力
灵感来自this answer ...
在阅读我写的答案时,您忽略了Trans
的一般质量,因为它是写在那里的。在这里,我们的Transduce
只尝试使用数组,但实际上它可以使用任何具有空值([]
)和concat
方法的类型。这两个属性构成了一个名为Monoids 的类别,如果我们不利用转换器处理该类别中任何类型的能力,我们就会给自己带来伤害。
在上面,我们在 run
方法中硬编码了初始累加器 []
,但这可能应该作为参数提供 - 就像我们对 iterable.reduce(reducer, initialAcc)
所做的那样
除此之外,这两种实现基本上是等效的。最大的区别是链接答案中提供的Trans
实现是Trans
本身是一个幺半群,但这里的Transduce
不是。 Trans
在concat
方法中巧妙地实现了传感器的组合,而Transduce
(上图)在每种方法中混合了组合。将其设为一个幺半群可以让我们以与所有其他幺半群相同的方式合理化 Trans
,而不必将其理解为具有独特的 map
、filter
和 run
方法的一些专用链接接口。
我建议从 Trans
构建而不是制作自己的自定义 API
把你的蛋糕也吃掉
所以我们学到了统一接口的宝贵经验,我们明白Trans
本质上很简单。但是,你仍然想要那个甜蜜的链接 API。好的,好的……
我们将再实现一次Transduce
,但这一次我们将使用Trans
幺半群来实现。这里,Transduce
持有一个 Trans
值而不是一个延续 (Function
)。
其他一切都保持不变 - foo
进行 1 个 微小 更改并产生相同的输出。
// generic transducers
const mapper = f =>
Trans(k => (acc, x) => k(acc, f(x)))
const filterer = f =>
Trans(k => (acc, x) => f(x) ? k(acc, x) : acc)
const logger = label =>
Trans(k => (acc, x) => (console.log(label, x), k(acc, x)))
// magic chaining api made with Trans monoid
const Transduce = (t = Trans.empty()) => (
map: f =>
Transduce(t.concat(mapper(f))),
filter: f =>
Transduce(t.concat(filterer(f))),
log: s =>
Transduce(t.concat(logger(s))),
run: (m, xs) =>
transduce(t, m, xs)
)
// when we run, we must specify the type to transduce
// .run(Array, nums)
// instead of
// .run(nums)
扩展此代码 sn-p 以查看最终实现 - 当然,您可以跳过单独定义 mapper
、filterer
和 logger
,而是直接在 Transduce
上定义它们。我认为这读起来更好。
// Trans monoid
const Trans = f => (
runTrans: f,
concat: (runTrans: g) =>
Trans(k => f(g(k)))
)
Trans.empty = () =>
Trans(k => k)
const transduce = (t, m, xs) =>
xs.reduce(t.runTrans((acc, x) => acc.concat(x)), m.empty())
// complete Array monoid implementation
Array.empty = () => []
// generic transducers
const mapper = f =>
Trans(k => (acc, x) => k(acc, f(x)))
const filterer = f =>
Trans(k => (acc, x) => f(x) ? k(acc, x) : acc)
const logger = label =>
Trans(k => (acc, x) => (console.log(label, x), k(acc, x)))
// now implemented with Trans monoid
const Transduce = (t = Trans.empty()) => (
map: f =>
Transduce(t.concat(mapper(f))),
filter: f =>
Transduce(t.concat(filterer(f))),
log: s =>
Transduce(t.concat(logger(s))),
run: (m, xs) =>
transduce(t, m, xs)
)
// this stays exactly the same
const foo = nums =>
return Transduce()
.log('greater than 2?')
.filter(x => x > 2)
.log('\tsquare:')
.map(x => x * x)
.log('\t\tless than 30?')
.filter(x => x < 30)
.log('\t\t\tpass')
.run(Array, nums)
// output is exactly the same
console.log(foo([1,2,3,4,5,6,7]))
// => [ 9, 16, 25 ]
总结
所以我们从一堆 lambda 开始,然后使用 monoid 让事情变得更简单。 Trans
monoid 提供了明显的优势,因为 monoid 接口是已知的,并且通用实现非常简单。但是我们很固执,或者我们可能有我们没有设定的目标要实现——我们决定构建神奇的 Transduce
链接 API,但我们使用坚如磐石的 Trans
单体来实现这一点,它为我们提供了所有的力量Trans
,但也很好地划分了复杂性。
点链恋物癖匿名者
这是我最近写的关于方法链接的其他几个答案
Is there any way to make a functions return accessible via a property? Chaining functions and using an anonymous function Pass result of functional chain to function【讨论】:
多么棒的答案!我曾希望通过跳过一些“抽象”内容并使代码不那么通用(即仅适用于数组)来让自己更轻松,但我现在看到它并不总是这样工作......我'不得不承认,您所指的一些概念(例如 η 转换和 Monoids)对我来说仍然难以理解。但我真的很感激你不只是把它留在快速修复(“但那是丑陋的” - 哈哈!)并且花时间一步一步地解释帮助你以结构化的方式解决这些问题的概念方式。 不错!请注意,eta 转换也适用于元组函数,无需柯里化它 @Bergi,我认为这有点不对劲。我的直觉说这应该是可能的,但是当我之前尝试的时候,我一定是犯了一个错误或什么的。感谢您发现它,它大大清理了答案。【参考方案2】:我认为您需要更改实现的顺序:
const filterer = pred => reducer => (x) =>pred((a=reducer(x) )?x: undefined;
const mapper = map => reducer => (x) => map(reducer(x));
那你需要把运行命令改成:
run: arr => arr.reduce((a,b)=>a.concat([reducer(b)]), []);
而且默认reducer必须是
x=>x
但是,这样过滤器将不起作用。您可以在 filter 函数中抛出 undefined 并在 run 函数中捕获:
run: arr => arr.reduce((a,b)=>
try
a.push(reducer(b));
catch(e)
return a;
, []);
const filterer = pred => reducer => (x) =>
if(!pred((a=reducer(x)))
throw undefined;
return x;
;
然而,总而言之,我认为 for 循环在这种情况下要优雅得多...
【讨论】:
我认为这不起作用,因为我的pred
是x -> bool
的函数,而map
是x -> y
。这使得减速器首先运行,返回一个数组(至少在我当前的示例中)。还是我在这里遗漏了什么?
@user3297291 你是对的。您还需要更改运行和默认减速器jsbin.com/laqezabihe/edit?console
问题的症结似乎真的在于支持filter
。我同意 for
循环选项胜过诉诸 try catch ()
...以上是关于如何以正确的顺序链接映射和过滤函数的主要内容,如果未能解决你的问题,请参考以下文章