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

6

我有一个简单的情况,需要过滤和映射到相同的值,就像这样:

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

为了节省代码行数并提高性能,我正在寻找以下内容:
const files = results.filterAndMap(function(r){
  return r.file;
});

这个功能是否已经存在,还是我需要自己编写?在几个地方我都想要这样的功能,只是以前从来没有去研究过。


results是什么?一个多维数组[{file:{file:1}}, {notfile:{file:1}}] - guest271314
结果只是一个对象数组:[{},{file:x}, {}, {file:y}],等等。 - Alexander Mills
结果只是一个对象数组:[{},{file:x}, {}, {file:y}]。嗯,这个数组与 JavaScript 中的上下文不匹配。在这种情况下,.map() 不是必需的。您可以仅使用 .filter() 来返回预期的结果。 - guest271314
抱歉,我没有理解您的评论。 - Alexander Mills
可能没有正确理解问题。最初将results解释为嵌套数组。为什么需要.map()?返回一个[x,y]数组?期望的结果是什么? - guest271314
使用只有一个Array.prototype方法的要求是什么? - guest271314
8个回答

10

变换器

回答你的问题,最通用的方式在于使用变换器(transducers)。但在我们深入抽象之前,让我们先了解一些基础知识——下面,我们实现了几个变换器mapReducefilterReducetapReduce;你可以添加任何其他你需要的变换器。

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 ]

可链式调用的API

也许您对代码的简洁性感到满意,但对略有不同寻常的API感到不满。如果您想保留链接.map.filter.whatever调用而不添加不必要的迭代,请使用转换的通用接口并在其上构建可链式API。本答案改编自我分享的链接和我关于转换器的其他答案

// 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 ]

可链式调用的API,第二部分

为了尽可能减少依赖仪式,在不依赖Trans单子实现或原始转换器mapperfilterer等的情况下,我重写了代码片段来实现链式调用API。感谢@ftor的评论。

总体易读性明显降低。我们失去了只需看一眼就能理解正在发生什么的能力。我们也失去了单子接口,这使得我们很容易在其他表达式中理解我们的转换器。但是,这里的一个巨大收获是Transduce的定义仅包含10行源代码;相比之前的28行,因此虽然表达式更加复杂,但您可能能够在您的大脑开始努力之前完成整个定义的阅读。

// 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 ]

> 关于性能的讨论

在速度方面,任何功能变体都不可能击败将所有程序语句合并在单个循环体中的静态for循环。然而,上述的转换器有潜力比多次迭代大型数据集的.map/.filter/.whatever调用更快。

编码风格和实现

转换器的本质在于mapReduce,这就是为什么我选择先介绍它的原因。如果你能理解如何将多个mapReduce调用串联起来,你就能理解转换器。

当然,你可以用任意数量的方法来实现转换器,但我发现Brian的方法最有用,因为它将转换器编码为单子——拥有单子使我们可以对其进行各种方便的假设。一旦我们转换了一个数组(一种类型的单子),你可能会想知道如何转换任何其他单子...在这种情况下,阅读那篇文章吧!


看起来很有趣,我会调查一下 :) - Alexander Mills
Brain谈论了逆变函子,并将它们与布尔幺半群结合起来,通过合取创建可组合的谓词。你通过实现一个幺半群传输器完成了他的博客文章 - 我不确定你的组合是否是逆变的。无论如何,这是一项杰出的工作(如果喜欢方法链接的话 - 我不喜欢:D如果可能的话,我会尝试在接下来的几天里简化它。 - user6445533
这个答案把我的Javascript智商提升了30分或者更多。哇! - adkisson

9
如果您确实需要在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方法更具表现力和可读性。

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()

你能把forEach添加到比较中吗? - ponury-kostek
@ponury-kostek 这是给你的 https://jsperf.com/filter-than-map-or-reduce/1 - 看起来在那个测试中 forEach 是最快的。 - Krzysztof Safjanowski
谢谢。我知道,这就是为什么他们试图用一些奇怪的方式来做这件事情让我感到惊讶。 - ponury-kostek
1
我猜测使用reduce选项的性能问题并不在于它使用了reduce,而是在于它使用了concat... 我经常使用(acc.push(x), acc)代替acc.concat(x)。这会改变acc数组,但由于该数组是在函数内部创建的,所以不应该是太大的问题。 - user3297291

2
为什么不只使用forEach呢?

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

如果速度还不够快,你可以使用fast.js并进行一些其他微小的优化。

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


1
您可以使用o.file的值或与空数组连接以获得结果。
results.reduce((r, o) => r.concat(o.file || []), []);

这是很多字符串拼接。 - Alexander Mills

0

数组是可迭代对象,我们可以在一次迭代中应用所有需要的操作。

下面的示例使用iter-ops库进行单次迭代:

import {pipe, filter, map} from 'iter-ops';

const i = pipe(
    results,
    filter(r => !!r.file),
    map(m => m.file)
);

console.log('files:', [...i]);

0

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

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


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

流程:

将 acc 初始化为空数组。缩减文档


1
"reduce" 操作比最便宜的操作更昂贵 - 正如 https://jsperf.com/filter-than-map-or-reduce/1 所证明的那样。 - Krzysztof Safjanowski
1
将 filter-then-map 与 reduce 进行比较,在您的答案中提供的截图中,@KrzysztofSafjanowski 显示了两者之间非常微小的差异。 - Heretic Monkey
@KrzysztofSafjanowski 哇,我的道歉,reduce更昂贵,谢谢提到我。感谢提到我,我一直认为reduce更便宜。 - ajilantang
@ajilantang 等几个v8迭代后,它可能就不会了。从逻辑上讲,reduce 应该更便宜,因为你只需要对数组进行一次迭代。我猜发生了以下两件事之一或两者都有:JIT正在优化 map/filter 操作中的一个传递或正在优化创建中间数据结构并生成更少垃圾的场合。 - Jared Smith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接