如何以正确的顺序链接地图和过滤器功能 [英] How to chain map and filter functions in the correct order
问题描述
我真的很喜欢链接 Array.prototype.map
, filter 和
reduce code>来定义数据转换。不幸的是,在最近涉及大型日志文件的项目中,我无法再多次循环访问数据...
我的目标:
我想创建一个链接 .filter
和 .map
方法,而不是立即在数组上映射,而是构成一个函数,该函数一次遍历数据 。 I b。:
const DataTransformation =()=> ({
map:fn =>(/ * ... * /),
filter:fn =>(/ * ... * /),
run:arr = >(/ * ... * /)
});
$ b $ const someTransformation = DataTransformation()
.map(x => x + 1)
.filter(x => x> 3)
。 map(x => x / 2);
//在
const myData = someTransformation.run([1,2])之间没有创建[2,3,4,5]和[4,5],返回[2,2.5] ,3,4]);
我的尝试:
受到< a href =https://stackoverflow.com/questions/44015014/mapping-an-object-to-array-of-objects-in-es6/44022167#44022167>这个答案和这篇博文我开始写一篇 Transduce
功能。
const filterer = pred =>减速器=> (acc,x)=>
pred(x)?减速机(acc,x):acc;
const mapper = map =>减速器=> (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
片段是它向后运行的......我链的最后一个方法是要执行的第一个方法:
const someTransformation = Transduce()
.map(x => x + 1)
.filter(x => x> 3 )
.map(x => x / 2);
//而不是[2,2.5]返回[]
//以(x / 2) - > [0.5,1,1.5,2]
//然后过滤器(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)))
类似于:
$ b $mapper(f)(mapper(g)(concat))
我想我明白为什么会发生这种情况,但我无法弄清楚如何在不改变接口。
问题:
如何让我的 之前,我们知道更好 我真的很喜欢链接...... 我明白了,我会安抚你,但你会明白,迫使你的程序通过一个链接API是不自然的,比大多数情况下它的价值更麻烦。按照正确的顺序转换方法链
过滤器
和映射
操作?
注意:
Transduce
字词或者有更好的方法来描述问题,请告诉我们。
作为
循环:
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}));返回{map:addAction(MAP),filter:addAction(FILTER),run};}; //比较常规链来检查//是否有性能增益//无可否认,在这个例子中,小... 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));
const filterer = pred =>减速器=> (acc,x)=> pred(x)? reducer(acc,x):acc; const mapper = map =>减速器=> (acc,x)=>减速器(acc,map(x)); const Transduce =(减速器=(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); //向后:// [-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]));
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
和过滤器
方法堆叠 map
和 pred
,而不是将它们嵌套在传感器链中。下面,我已经实现了您的 Transduce
以正确顺序评估贴图和过滤器的API。我还添加了一个 log
方法,以便我们可以看到 Transduce
是如何运作的
((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 = (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)} //保持方形(n),所有n个数//其中n> 2 //其中square(n)< 30console.log(foo([1,2,3,4,5,6,7]))// => [9,16,25]
尚未挖掘的潜力
受到这个答案 ...
在阅读我写的答案时,忽略了 上面,我们对初始累加器 Trans 的通用质量。在这里,我们的
方法,但这应该可以作为参数提供 - 就像我们用 Transduce
只会尝试使用数组,但实际上它可以处理任何具有空值的类型( []
)和一个 concat
方法。这两个属性组成了一个名为 Monoid 的类别,如果我们自己做了一些破坏没有利用传感器的能力来处理这个类别中的任何类型。
[]
运行 iterable.reduce(reducer ,initialAcc)
除此之外,两个实现基本上是等价的。最大的区别是链接答案中提供的 我会建议从 有你的蛋糕并且也吃它所以我们学到了统一接口的宝贵经验,我们知道 code> Trans Trans
实现是 Trans 本身是一个monoid,但是
本质上很简单。但是,你仍然需要这个甜蜜的链接API。好的,好的... Transduce
这里不是。 Trans
整齐地在 concat
方法中实现换能器的组合,而 Transduce
(以上)在每种方法中混合了组合。使它成为一个monoid允许我们以与所有其他monoids相同的方式合理化 Trans
,而不必将其理解为一些具有唯一映射的专用链接接口
,过滤器
和运行
方法。
Trans
建设,而不是制作自己的自定义API
我们将再次执行 其他一切保持不变 - 展开此代码当然你可以跳过定义一个单独的 换行 因此,我们开始使用一堆lambda表达式,然后使用monoid使事情变得更简单。 Transduce
,但这次我们'我们将使用 Trans
monoid来做到这一点。在这里, Transduce
持有 Trans
值而不是延续( Function
链接API,但我们使用我们的摇滚乐游戏, solid
foo
需要1 微小更改并产生相同的输出。
//通用换能器
常量映射器= 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与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)=>
转换(t,m,xs)
})
//当我们运行时,我们必须指定类型来转换
// .run(Array,nums)
//而不是
// .run(nums)
mapper
, filterer
和 logger
,而是直接在 Transduce
上定义它们。我认为这会更好地阅读。
// Trans monoidconst Trans = f => ({runTrans:f,concat:({runTrans:g})=> Trans(k => f(g(k)))})Trans.empty =()=> Trans(k => k)常数转换=(t,m,xs)=> xs.reduce(t.runTrans((acc,x)=> acc.concat(x)),m.empty())//完整数组monoid implementationArray.empty =()=> [] // generic transducersconst 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)))//现在用Trans monoidconst Transduce =(t = Trans.empty()) => (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)})//这个保持完全相同的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)} //输出是完全相同的console.log(foo([1,2,3,4,5,6,7]))// => [9,16,25]
Trans
monoid提供了独特的优势,因为monoid接口是已知的,并且通用实现非常简单。但我们固执己见,也许我们有目标要实现,而不是由我们设定 - 我们决定建立魔术 Trans
monoid给了我们 Trans 的所有功能,但也保持了复杂度的精细划分。
连环恋物癖匿名
我写的关于方法链接的最近的答案
- 有什么办法使一个函数返回可以通过属性?
- 链接功能并使用匿名功能
-
I really like chaining
Array.prototype.map
,filter
andreduce
to define a data transformation. Unfortunately, in a recent project that involved large log files, I could no longer get away with looping through my data multiple times...My goal:
I want to create a function that chains
.filter
and.map
methods by, instead of mapping over an array immediately, composing a function that loops over the data once. I.e.: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]);
My attempt:
Inspired by this answer and this blogpost I started writing a
Transduce
function.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, []) });
The problem:
The problem with the
Transduce
snippet above, is that it runs "backwards"... The last method I chain is the first to be executed: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]);
Or, in more abstract terms:
Go from:
Transducer(concat).map(f).map(g) == (acc, x) => concat(acc, f(g(x)))
To:
Transducer(concat).map(f).map(g) == (acc, x) => concat(acc, g(f(x)))
Which is similar to:
mapper(f) (mapper(g) (concat))
I think I understand why it happens, but I can't figure out how to fix it without changing the "interface" of my function.
The question:
How can I make my
Transduce
method chainfilter
andmap
operations in the correct order?
Notes:
- I'm only just learning about the naming of some of the things I'm trying to do. Please let me know if I've incorrectly used the
Transduce
term or if there are better ways to describe the problem. - I'm aware I can do the same using a nested
for
loop:
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));
- Here's my attempt in a stack snippet:
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]));
解决方案before we know better
I really like chaining ...
I see that, and I'll appease you, but you'll come to understand that forcing your program through a chaining API is unnatural, and more trouble than it's worth in most cases.
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, []) });
I think I understand why it happens, but I can't figure out how to fix it without changing the "interface" of my function.
The problem is indeed with your
Transduce
constructor. Yourmap
andfilter
methods are stackingmap
andpred
on the outside of the transducer chain, instead of nesting them inside.Below, I've implemented your
Transduce
API that evaluates the maps and filters in correct order. I've also added alog
method so that we can see howTransduce
is behavingconst 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 ]
untapped potential
Inspired by this answer ...
In reading that answer I wrote, you overlook the generic quality of
Trans
as it was written there. Here, ourTransduce
only attempts to work with Arrays, but really it can work with any type that has an empty value ([]
) and aconcat
method. These two properties make up a category called Monoids and we'd be doing ourselves a disservice if we didn't take advantage of transducer's ability to work with any type in this category.Above, we hard-coded the initial accumulator
[]
in therun
method, but this should probably be supplied as an argument – much like we do withiterable.reduce(reducer, initialAcc)
Aside from that, both implementations are essentially equivalent. The biggest difference is that the
Trans
implementation provided in the linked answer isTrans
itself is a monoid, butTransduce
here is not.Trans
neatly implements composition of transducers in theconcat
method whereasTransduce
(above) has composition mixed within each method. Making it a monoid allows us to rationalizeTrans
the same way do all other monoids, instead of having to understand it as some specialized chaining interface with uniquemap
,filter
, andrun
methods.I would advise building from
Trans
instead of making your own custom API
have your cake and eat it too
So we learned the valuable lesson of uniform interfaces and we understand that
Trans
is inherently simple. But, you still want that sweet chaining API. OK, ok...We're going to implement
Transduce
one more time, but this time we'll do so using theTrans
monoid. Here,Transduce
holds aTrans
value instead of a continuation (Function
).Everything else stays the same –
foo
takes 1 tiny change and produces an identical output.// 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)
Expand this code snippet to see the final implementation – of course you could skip defining a separate
mapper
,filterer
, andlogger
, and instead define those directly onTransduce
. I think this reads nicer tho.// 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 ]
wrap up
So we started with a mess of lambdas and then made things simpler using a monoid. The
Trans
monoid provides distinct advantages in that the monoid interface is known and the generic implementation is extremely simple. But we're stubborn or maybe we have goals to fulfill that are not set by us – we decide to build the magicTransduce
chaining API, but we do so using our rock-solidTrans
monoid which gives us all the power ofTrans
but also keeps complexity nicely compartmentalised.
dot chaining fetishists anonymous
Here's a couple other recent answers I wrote about method chaining
- 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
这篇关于如何以正确的顺序链接地图和过滤器功能的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!
- I'm only just learning about the naming of some of the things I'm trying to do. Please let me know if I've incorrectly used the