如何以正确的顺序链接映射和过滤函数

IT技术 javascript functional-programming
2021-03-02 21:18:45

我真的很喜欢链接Array.prototype.mapfilterreduce定义数据转换。不幸的是,在最近的一个涉及大型日志文件的项目中,我无法再多次循环遍历我的数据...

我的目标:

我想创建一个函数来链接.filter.map方法,而不是立即映射数组,而是组成一个循环遍历数据一次的函数IE:

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

我的尝试:

受到这个答案这篇博文的启发,我开始编写一个Transduce函数。

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, [])
});

问题:

Transduce上面代码片段的问题在于它“向后”运行……我链接的最后一个方法是第一个执行的:

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

或者,用更抽象的术语:

从...来:

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

这类似于:

mapper(f) (mapper(g) (concat))

我想我明白为什么会发生,但我无法弄清楚如何在不改变我的函数的“接口”的情况下修复它。

问题:

如何以正确的顺序制作我的Transduce方法链filtermap操作?


笔记:

  • 我只是在学习我正在尝试做的一些事情的命名。如果我错误地使用了该Transduce术语,或者是否有更好的方法来描述问题,请告诉我
  • 我知道我可以使用嵌套for循环来做同样的事情

  • 这是我在堆栈片段中的尝试:

2个回答

在我们更好地了解之前

我真的很喜欢链...

我明白了,我会安抚你,但你会明白强制你的程序通过链接 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构造函数上。您的mapfilter方法在换能器链的外部堆叠mappred放置,而不是将它们嵌套在内部。

下面,我已经实现了您的TransduceAPI,它以正确的顺序评估地图和过滤器。我还添加了一个log方法,以便我们可以查看Transduce行为

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 ]


未开发的潜力

受到这个答案的启发......

在阅读我写的那个答案时,你忽略了Trans它写在那里的一般质量在这里,我们Transduce只尝试使用数组,但实际上它可以使用任何具有空值 ( []) 和concat方法的类型。这两个属性构成了一个称为Monoids的类别,如果我们不利用转换器处理该类别中任何类型的能力,我们会对自己造成伤害。

上面,我们[]run方法中硬编码了初始累加器,但这可能应该作为参数提供——就像我们做的一样iterable.reduce(reducer, initialAcc)

除此之外,两种实现本质上是等效的。最大的区别是Trans链接答案中提供实现Trans本身是一个幺半群,但Transduce这里不是。Transconcat方法中巧妙地实现了换能器的组合,Transduce(上图)在每个方法中混合了组合。使其成为一个独异使我们能够合理化Trans以同样的方式做所有其他类群,而不必把它理解为一些专门的链接接口,具有唯一,mapfilter,和run方法。

我建议从构建Trans而不是创建自己的自定义 API


有你的蛋糕,也吃它

所以我们学到了统一接口的宝贵教训,我们明白这Trans本质上很简单。但是,您仍然需要那个甜蜜的链接 API。好的好的...

我们将再实现Transduce一次,但这次我们将使用Trans幺半群来实现。在这里,Transduce持有一个Trans值而不是一个延续 ( Function)。

其他一切都保持不变——foo只需进行 1 次微小的更改,即可产生相同的输出。

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

展开这个代码片段,看看最终的实现-当然,你可以跳过定义一个独立的mapperfiltererlogger,而是直接在定义这些Transduce我认为这读起来更好。


包起来

所以我们从一堆 lambda 开始,然后使用幺半群使事情变得更简单。Trans半群,所述半群接口是已知的提供不同的优点和通用的实现是非常简单的。但是我们很顽固,或者我们有一些目标要实现,而这些目标不是由我们设定的——我们决定构建神奇的Transduce链式 API,但我们使用坚如磐石的Trans幺半群来实现,这为我们提供了所有的力量,Trans但也很好地保持了复杂性划分的。


点链恋物癖匿名

这是我最近写的关于方法链的其他几个答案

@Bergi,我认为这有什么不对的。我的直觉说它应该是可能的,但是当我早些时候尝试时,我一定是犯了错误或什么的。感谢您抓住这一点,它大大清理了答案。
2021-04-21 21:18:45
多么棒的答案!我曾希望通过跳过一些“抽象”的东西并使代码不那么通用(即仅适用于数组)来使事情变得更容易,但我现在看到它并不总是那样工作......我'不得不承认,您所指的某些概念(如 η 转换和 Monoids)对我来说仍然难以掌握。但我真的很感激你没有把它放在快速解决方案上(“但这太丑了”——哈哈!)并且花时间一步一步地解释帮助你解决此类问题的概念。大大地。
2021-04-22 21:18:45
好的!请注意,eta 转换也适用于元组函数,无需对其进行柯里化
2021-04-22 21:18:45

我认为你需要改变你的实现顺序:

const filterer = pred => reducer => (x) =>pred((a=reducer(x) )?x: undefined;

const mapper = map => reducer => (x) => map(reducer(x));

然后你需要将运行命令更改为:

run: arr => arr.reduce((a,b)=>a.concat([reducer(b)]), []);

并且默认的减速器必须是

x=>x

但是,这样过滤器就不起作用了。您可以在 filter 函数中抛出 undefined 并在 run 函数中捕获:

run: arr => arr.reduce((a,b)=>{
try{
 a.push(reducer(b));
}catch(e){}
return a;
}, []);

const filterer = pred => reducer => (x) =>{
 if(!pred((a=reducer(x))){
   throw undefined;
 }
 return x;
};

但是,总而言之,我认为在这种情况下 for 循环要优雅得多......

问题的关键实际上是在支持filter它。我同意for循环选项胜过不得不求助于try {} catch () {}......
2021-04-28 21:18:45
我不认为这个作品,因为我pred是一个功能x -> boolmapx -> y这使减速器首先运行,返回一个数组(至少在我当前的示例中)。还是我在这里遗漏了什么?
2021-04-29 21:18:45
@ user3297291 你是对的。您还需要更改运行和默认减速器jsbin.com/laqezabihe/edit?console
2021-05-14 21:18:45