【问题标题】:filter and map in same iteration在同一迭代中过滤和映射
【发布时间】:2017-12-22 19:32:11
【问题描述】:

我有一个简单的情况,我想过滤并映射到相同的值,如下所示:

 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}}]?
  • results 只是一个对象数组:[{},{file:x}, {}, {file:y}]
  • "results 只是一个对象数组:[{},{file:x}, {}, {file:y}]" 那么,该数组与 Question 中的 JavaScript 上下文不匹配。在这种情况下,.map() 是不必要的。您可以单独使用.filter() 来返回预期结果
  • 对不起,我没有关注你的评论
  • 可能没有正确解释问题。最初将 results 解释为嵌套数组。为什么需要.map()?返回[x, y]的数组?预期的结果是什么?

标签: javascript node.js functional-programming


【解决方案1】:

传感器

以最通用的形式,您的问题的答案在于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 - 这个答案改编自我在上面分享的链接和other answers I have about transducers

// 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,取 2

作为以尽可能少的依赖仪式实现链接 API 的练习,我重写了代码 sn-p 而不依赖于 Trans 半群实现或原始传感器 mapperfilterer 等 - 谢谢对于@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's approach 最有用,因为它将传感器编码为monoid——拥有一个幺半群允许我们对它做出各种方便的假设。一旦我们转换了一个数组(一种幺半群),您可能想知道如何转换任何其他幺半群……在这种情况下,请阅读那篇文章!

【讨论】:

  • 看起来很有趣,会调查 :)
  • Brain 谈论逆变函子并将它们与布尔幺半群结合起来以创建可组合的谓词。您实际上是通过实现一个单曲面转换器完成了他的博客文章——不过我不确定您的组合是否是逆变的。无论如何,出色的工作(如果有人喜欢方法链接-我不喜欢:D如果有可能的话,我将在接下来的几天内尝试简化它。
  • 这个答案将我的 Javascript IQ 提高了 30 分或更多。哇!
【解决方案2】:

如果您真的需要在 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

    欢迎任何其他测试用例。

    如果你想使用 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 你是jsperf.com/filter-than-map-or-reduce/1 - 看起来 forEach 是该挑战中最快的
    • 谢谢。我知道,这就是为什么我很惊讶他们试图以一些奇怪的方式做到这一点。
    • 我猜reduce 选项的性能问题并不是因为它使用了reduce,而是因为它使用了concat...使用(acc.push(x), acc) 而不是acc.concat(x)。它会改变 acc 数组,但由于数组是在您的函数内部创建的,因此问题应该不会太大。
    【解决方案4】:

    为什么不只是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;
      }
    }

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

      • 有很多连接
      【解决方案6】:

      您可以使用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

      【讨论】:

        【解决方案7】:

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

        下面的示例使用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]);
        

        【讨论】:

          【解决方案8】:
             const file = (array) => {
               return array.reduce((acc,curr) => curr.file ? acc.concat(curr) : acc, 
               [])
              } 
          

          流程:

          acc 初始化为 [ ] (空数组) 。 reduce docs

          【讨论】:

          • reduce 是比最便宜的操作最昂贵的操作 - 正如 jsperf.com/filter-than-map-or-reduce/1 所证明的那样
          • 比较 filter-then-map 以减少,至少在您的答案中提供的屏幕截图中,@KrzysztofSafjanowski 显示两者之间的差异非常小。
          • @KrzysztofSafjanowski 哇,我的道歉减少了,谢谢你提到我。谢谢你提到我,我在想减少更便宜。
          • @ajilantang 再等几个 v8 迭代,它可能不会。从逻辑上讲,reduce 应该更便宜,因为您只迭代数组一次。我猜想发生两件事中的一件事或两件事:JIT 正在优化 map/filter 案例中的一个传递和/或正在优化中间数据结构的创建并生成更少的垃圾。
          猜你喜欢
          • 1970-01-01
          • 2015-08-30
          • 1970-01-01
          • 2020-04-18
          • 2016-08-22
          • 2016-07-05
          • 1970-01-01
          • 1970-01-01
          • 2021-08-21
          相关资源
          最近更新 更多