如何以正确的顺序链接地图和过滤器功能 [英] How to chain map and filter functions in the correct order

查看:107
本文介绍了如何以正确的顺序链接地图和过滤器功能的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我真的很喜欢链接 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))


我想我明白为什么会发生这种情况,但我无法弄清楚如何在不改变接口。



问题:



如何让我的按照正确的顺序转换方法链过滤器映射操作?




注意:




  • 我只是在了解我想要做的一些事情的命名。如果我错误地使用了 Transduce 字词或者有更好的方法来描述问题,请告诉我们。

  • m知道我可以使用一个嵌套的作为循环:


  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]));  

解决方案

之前,我们知道更好 我真的很喜欢链接......


我明白了,我会安抚你,但你会明白,迫使你的程序通过一个链接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 过滤器方法堆叠 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)



除此之外,两个实现基本上是等价的。最大的区别是链接答案中提供的 Trans 实现是 Trans 本身是一个monoid,但是 Transduce 这里不是。 Trans 整齐地在 concat 方法中实现换能器的组合,而 Transduce (以上)在每种方法中混合了组合。使它成为一个monoid允许我们以与所有其他monoids相同的方式合理化 Trans ,而不必将其理解为一些具有唯一映射的专用链接接口过滤器运行方法。

我会建议从 Trans 建设,而不是制作自己的自定义API






有你的蛋糕并且也吃它所以我们学到了统一接口的宝贵经验,我们知道

code> Trans 本质上很简单。但是,你仍然需要这个甜蜜的链接API。好的,好的...



我们将再次执行 Transduce ,但这次我们'我们将使用 Trans monoid来做到这一点。在这里, Transduce 持有 Trans 值而不是延续( Function

其他一切保持不变 - 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]






换行

因此,我们开始使用一堆lambda表达式,然后使用monoid使事情变得更简单。 Trans monoid提供了独特的优势,因为monoid接口是已知的,并且通用实现非常简单。但我们固执己见,也许我们有目标要实现,而不是由我们设定 - 我们决定建立魔术 Transduce 链接API,但我们使用我们的摇滚乐游戏, solid Trans monoid给了我们 Trans 的所有功能,但也保持了复杂度的精细划分。






连环恋物癖匿名

我写的关于方法链接的最近的答案


  • 有什么办法使一个函数返回可以通过属性?

  • 链接功能并使用匿名功能


  • I really like chaining Array.prototype.map, filter and reduce 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 chain filter and map 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. Your map and filter methods are stacking map and pred 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 a log method so that we can see how Transduce is behaving

    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 ]


    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, our Transduce only attempts to work with Arrays, but really it can work with any type that has an empty value ([]) and a concat 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 the run method, but this should probably be supplied as an argument – much like we do with iterable.reduce(reducer, initialAcc)

    Aside from that, both implementations are essentially equivalent. The biggest difference is that the Trans implementation provided in the linked answer is Trans itself is a monoid, but Transduce here is not. Trans neatly implements composition of transducers in the concat method whereas Transduce (above) has composition mixed within each method. Making it a monoid allows us to rationalize Trans the same way do all other monoids, instead of having to understand it as some specialized chaining interface with unique map, filter, and run 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 the Trans monoid. Here, Transduce holds a Trans 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, and logger, and instead define those directly on Transduce. 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 magic Transduce chaining API, but we do so using our rock-solid Trans monoid which gives us all the power of Trans but also keeps complexity nicely compartmentalised.


    dot chaining fetishists anonymous

    Here's a couple other recent answers I wrote about method chaining

    这篇关于如何以正确的顺序链接地图和过滤器功能的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆