在同一次迭代中过滤和映射

时间:2017-07-18 05:24:44

标签: javascript node.js functional-programming

我有这种简单的情况,我希望过滤并映射到相同的值,如下所示:

 const files = results.filter(function(r){
      return r.file;
    })
    .map(function(r){
       return r.file;
    });

为了保存代码行,以及提高性能,我正在寻找:

const files = results.filterAndMap(function(r){
  return r.file;
});

这是否存在,或者我应该自己写点什么?我想在几个地方使用这样的功能,从来没有费心去研究它。

7 个答案:

答案 0 :(得分:8)

如果您确实需要在1个功能中执行此操作,则需要使用reduce这样的

results.reduce(
  // add the file name to accumulator if it exists
  (acc, result) => result.file ? acc.concat([result.file]) : acc,
  // pass empty array for initial accumulator value
  []
)

如果您需要提高性能,可以将concat更改为push并返回原始累加器数组,以避免创建额外的数组。

但是,最快的解决方案可能是一个很好的旧for循环,它可以避免所有函数调用和堆栈帧

files = []
for (var i = 0; i < results.length; i++) {
  var file = results[i].file
  if (file) files.push(file)
}

但我认为filter/map方法更具表现力和可读性

答案 1 :(得分:7)

<强>传感器

以最通用的形式,您的问题的答案在于transducers。但在我们过于抽象之前,让我们先看一些基础 - 下面,我们实现了几个传感器mapReducefilterReducetapReduce;你可以添加你需要的任何其他东西。

&#13;
&#13;
const mapReduce = map => reduce =>
  (acc, x) => reduce (acc, map (x))
  
const filterReduce = filter => reduce =>
  (acc, x) => filter (x) ? reduce (acc, x) : acc
  
const tapReduce = tap => reduce =>
  (acc, x) => (tap (x), reduce (acc, x))

const tcomp = (f,g) =>
  k => f (g (k))

const concat = (xs,ys) =>
  xs.concat(ys)
  
const transduce = (...ts) => xs =>
  xs.reduce (ts.reduce (tcomp, k => k) (concat), [])

const main =
  transduce (
    tapReduce (x => console.log('with:', x)),
    filterReduce (x => x.file),
    tapReduce (x => console.log('has file:', x.file)),
    mapReduce (x => x.file),
    tapReduce (x => console.log('final:', x)))
      
const data =
  [{file: 1}, {file: undefined}, {}, {file: 2}]
  
console.log (main (data))
// with: { file: 1 }
// has file: 1
// final: 1
// with: { file: undefined }
// with: {}
// with: { file: 2 }
// has file: 2
// final: 2
// => [ 1, 2 ]
&#13;
&#13;
&#13;

可连接API

也许您对代码的简单性感到满意,但您对某些非传统的API感到不满意。如果您希望保留链接.map.filter.whatever调用而无需添加过度迭代的功能,我们可以创建一个通用接口来进行转换,并在此基础上创建可链接的API - 这个答案改编自我上面分享的链接和other answers I have about transducers

&#13;
&#13;
// Trans Monoid
const Trans = f => ({
  runTrans: f,
  concat: ({runTrans: g}) =>
    Trans (k => f (g (k)))
})

Trans.empty = () =>
  Trans(k => k)

// transducer "primitives"
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 tapper = f =>
  Trans (k => (acc, x) => (f (x), k (acc, x)))
  
// chainable API
const Transduce = (t = Trans.empty()) => ({
  map: f =>
    Transduce (t.concat (mapper (f))),
  filter: f =>
    Transduce (t.concat (filterer (f))),
  tap: f =>
    Transduce (t.concat (tapper (f))),
  run: xs =>
    xs.reduce (t.runTrans ((xs,ys) => xs.concat(ys)), [])
})

// demo
const main = data =>
  Transduce()
    .tap (x => console.log('with:', x))
    .filter (x => x.file)
    .tap (x => console.log('has file:', x.file))
    .map (x => x.file)
    .tap (x => console.log('final:', x))
    .run (data)
    
const data =
  [{file: 1}, {file: undefined}, {}, {file: 2}]

console.log (main (data))
// with: { file: 1 }
// has file: 1
// final: 1
// with: { file: undefined }
// with: {}
// with: { file: 2 }
// has file: 2
// final: 2
// => [ 1, 2 ]
&#13;
&#13;
&#13;

可连接的API,需要2

作为使用尽可能少的依赖仪式来实现链接API的练习,我重写了代码片段而不依赖于Trans幺半群实现或原始传感器mapperfilterer等等 - 感谢评论@ftor。

就整体可读性而言,这是一个明确的降级。我们失去了只看它并理解发生了什么的能力。我们也丢失了monoid接口,这使我们很容易在其他表达式中推断出我们的传感器。这里有一个很大的好处是Transduce的定义包含在10行源代码中;与之前的28个相比 - 所以虽然表达式更复杂,但你可以在大脑开始挣扎之前完成阅读整个定义

&#13;
&#13;
// chainable API only (no external dependencies)
const Transduce = (t = k => k) => ({
  map: f =>
    Transduce (k => t ((acc, x) => k (acc, f (x)))),
  filter: f =>
    Transduce (k => t ((acc, x) => f (x) ? k (acc, x) : acc)),
  tap: f =>
    Transduce (k => t ((acc, x) => (f (x), k (acc, x)))),
  run: xs =>
    xs.reduce (t ((xs,ys) => xs.concat(ys)), [])
})

// demo (this stays the same)
const main = data =>
  Transduce()
    .tap (x => console.log('with:', x))
    .filter (x => x.file)
    .tap (x => console.log('has file:', x.file))
    .map (x => x.file)
    .tap (x => console.log('final:', x))
    .run (data)
    
const data =
  [{file: 1}, {file: undefined}, {}, {file: 2}]

console.log (main (data))
// with: { file: 1 }
// has file: 1
// final: 1
// with: { file: undefined }
// with: {}
// with: { file: 2 }
// has file: 2
// final: 2
// => [ 1, 2 ]
&#13;
&#13;
&#13;

<强>&GT;谈论绩效

在速度方面,没有任何功能变体能够击败静态for循环,它将所有程序语句组合在一个循环体中。但是,上面的传感器确实 potential 要比一系列.map / .filter / .whatever调用快,其中通过大数据集进行多次迭代会很贵。

编码风格&amp;实施

传感器的本质在于mapReduce,这就是我选择首先引入它的原因。如果您能够理解如何进行多个mapReduce调用并将它们排列在一起,那么您将了解传感器。

当然你可以通过多种方式实现传感器,但我发现Brian's approach最有用,因为它将传感器编码为monoid - 有一个monoid允许我们做出各种方便的假设它。一旦我们转换了一个阵列(一种类型的幺半群),你可能想知道如何转换任何其他幺半群...在这种情况下,阅读那篇文章!

答案 2 :(得分:3)

要提高性能,您必须更快地衡量哪种解决方案。让我们玩一会儿https://jsperf.com/filter-than-map-or-reduce/1

欢迎任何其他测试用例。

enter image description here

如果你想使用针对NodeJS的基准测试(请记住npm i benchmark

var suite = new (require('benchmark')).Suite

function getSampleInput() {
  return [{file: 'foo'}, {other: 'bar'}, {file: 'baz'}, {file: 'quux'}, {other: 'quuxdoo'}, {file: 'foobar'}, {file: 'foo'}, {other: 'bar'}, {file: 'baz'}, {file: 'quux'}, {other: 'quuxdoo'}, {file: 'foobar'}, {file: 'foo'}, {other: 'bar'}, {file: 'baz'}, {file: 'quux'}, {other: 'quuxdoo'}, {file: 'foobar'}, {file: 'foo'}, {other: 'bar'}, {file: 'baz'}, {file: 'quux'}, {other: 'quuxdoo'}, {file: 'foobar'}, {file: 'foo'}, {other: 'bar'}, {file: 'baz'}, {file: 'quux'}, {other: 'quuxdoo'}, {file: 'foobar'}, {file: 'foo'}, {other: 'bar'}, {file: 'baz'}, {file: 'quux'}, {other: 'quuxdoo'}, {file: 'foobar'}, {file: 'foo'}, {other: 'bar'}, {file: 'baz'}, {file: 'quux'}, {other: 'quuxdoo'}, {file: 'foobar'}, {file: 'foo'}, {other: 'bar'}, {file: 'baz'}, {file: 'quux'}, {other: 'quuxdoo'}, {file: 'foobar'}]
}

// author https://stackoverflow.com/users/3716153/gaafar 
function reduce(results) {
  return results.reduce(
    (acc, result) => result.file ? acc.concat([result.file]) : acc ,
    []
  )  
}

// author https://stackoverflow.com/users/1223975/alexander-mills
function filterThanMap(results) {
  return results.filter(function(r){
    return r.file;
  })
  .map(function(r){
     return r.file;
  });
}

// author https://stackoverflow.com/users/5361130/ponury-kostek
function forEach(results) {
  const files = [];

  results.forEach(function(r){
    if(r.file) files.push(r.file); 
  });

  return files
}

suite
  .add('filterThanMap', function() {filterThanMap(getSampleInput())})
  .add('reduce', function() {reduce(getSampleInput())})
  .add('forEach', function() {forEach(getSampleInput())})
  .on('complete', function() {
    console.log('results:')
    this.forEach(function(result) {
      console.log(result.name, result.count, result.times.elapsed)
    })
    console.log('the fastest is', this.filter('fastest').map('name')[0])
  })
  .run()

答案 3 :(得分:2)

为什么不只是forEach

&#13;
&#13;
const files = [];
results.forEach(function(r){
  if(r.file) {
    files.push(r.file);  
  }
});
&#13;
&#13;
&#13;

如果速度不够快,您可以使用fast.js并进行其他微观优化

&#13;
&#13;
const files = [];
const length = results.length;
for(var i = 0; i < length; i++) {
  if (results[i].file) {
    files[files.length] = results[i].file;
  }
}
&#13;
&#13;
&#13;

答案 4 :(得分:1)

您可以使用o.file的值或者使用空数组连接结果。

results.reduce((r, o) => r.concat(o.file || []), []);

答案 5 :(得分:0)

您可以使用Array.prototype.reduce()

&#13;
&#13;
const results = [{file:{file:1}}, {notfile:{file:1}}];

const files = results.reduce(function(arr, r){
                return r.file ? arr = [...arr, r.file.file] : arr;
              }, []);
              
console.log(files); // 1
&#13;
&#13;
&#13;

答案 6 :(得分:-2)

   const file = (array) => {
     return array.reduce((acc,curr) => curr.file ? acc.concat(curr) : acc, 
     [])
    } 

流程:

acc作为[](空数组)启动。 reduce docs