【问题标题】:Ramda.js transducers: average the resulting array of numbersRamda.js 转换器:平均结果数组
【发布时间】:2021-09-15 18:00:51
【问题描述】:

我目前正在学习使用 Ramda.js 的传感器。 (太有趣了,耶!????)

我发现this question that 描述了如何首先过滤一个数组,然后使用转换器对其中的值求和。

我想做一些类似但不同的事情。我有一组具有时间戳的对象,我想平均时间戳。像这样的:

const createCheckin = ({
  timestamp = Date.now(), // default is now
  startStation = 'foo',
  endStation = 'bar'
} = {}) => ({timestamp, startStation, endStation});

const checkins = [
  createCheckin(),
  createCheckin({ startStation: 'baz' }),
  createCheckin({ timestamp: Date.now() + 100 }), // offset of 100
];

const filterCheckins = R.filter(({ startStation }) => startStation === 'foo');
const mapTimestamps = R.map(({ timestamp }) => timestamp);

const transducer = R.compose(
  filterCheckins,
  mapTimestamps,
);

const average = R.converge(R.divide, [R.sum, R.length]);

R.transduce(transducer, average, 0, checkins);
// Should return something like Date.now() + 50, giving the 100 offset at the top.

当然,上面提到的average 不能工作,因为转换函数的工作方式类似于 reduce。

我发现我可以在换能器之后一步完成。

const timestamps = R.transduce(transducer,  R.flip(R.append), [], checkins);
average(timestamps);

但是,我认为必须有一种方法可以使用迭代器函数(转换器的第二个参数)来做到这一点。你怎么能做到这一点?或者average 必须是transducer 的一部分(使用compose)?

【问题讨论】:

    标签: javascript average ramda.js transducer


    【解决方案1】:

    作为第一步,您可以创建一个简单的类型以允许合并平均值。这需要保持对被平均的项目的总和和数量进行统计。

    const Avg = (sum, count) => ({ sum, count })
    
    // creates a new `Avg` from a given value, initilised with a count of 1
    Avg.of = n => Avg(n, 1)
    
    // takes two `Avg` types and combines them together
    Avg.append = (avg1, avg2) =>
      Avg(avg1.sum + avg2.sum, avg1.count + avg2.count)
    

    有了这个,我们可以将注意力转移到创建将合并平均值的转换器。

    首先,一个简单的辅助函数允许将值转换为我们的 Avg 类型,并且还包装了一个 reduce 函数以默认为它接收到的第一个值,而不是要求提供初始值(一个好的初始值不会'不存在平均值,所以我们将只使用第一个值)

    const mapReduce1 = (map, reduce) =>
      (acc, n) => acc == null ? map(n) : reduce(acc, map(n))
    

    然后,转换器只需将Avg 值组合起来,然后将结果平均值从结果中提取出来。注意在转换器运行在空列表上的情况下,结果需要保护null 值。

    const avgXf = {
      '@@transducer/step': mapReduce1(Avg.of, Avg.append),
      '@@transducer/result': result =>
        result == null ? null : result.sum / result.count
    }
    

    然后您可以将其作为累加器函数传递给transduce,它应该会产生结果平均值。

    transduce(transducer, avgXf, null, checkins)
    

    【讨论】:

    • 现在我怎么没想到??? :-) 我仍然没有完全关注(这里的transducer 是什么?)而且我不确定它是否那么重要。我确实觉得你可以用Avg (0, 0)result => result.count == 0 ? 0 : result.sum / result.count 进行初始化,尽管我很可能会遗漏一些东西。我认为就像空列表的总和在逻辑上定义为零一样,空列表的平均值也可以定义为零。但很明显,这更多的是学术而非实际利益。尽管如此,还是很有趣!
    【解决方案2】:

    恐怕这让我很困惑。

    我认为传感器是一种将组合函数的步骤组合到值序列上的一种方式,这样您就可以只迭代序列一次。

    average 在这里毫无意义。要取平均值,您需要整个系列。

    因此您可以转换值的过滤和映射。但是您绝对需要单独进行平均。请注意,filter 然后map 是一种足够常见的模式,周围有很多filterMap 函数。 Ramda 没有,但这会很好:

    const filterMap = (f, m) => (xs) =>
      xs .flatMap (x => f (x) ? [m (x)] : [])
    

    然后会像这样使用:

    filterMap (
      propEq ('startStation', 'foo'), 
      prop ('timestamp')
    ) (checkins)
    

    但对于更复杂的转换序列,转换器当然可以满足要求。


    我还建议尽可能使用lift 而不是converge。它是一个更标准的 FP 函数,并且适用于更抽象的数据类型。这里const average = lift (divide) (sum, length) 可以正常工作。

    【讨论】:

    • 您只需要整个集合,就像您需要整个集合来确定列表的大小一样。也就是说,您可以创建一个类型,该类型收集要平均的值的总和的元组,以及要平均的值的计数,允许平均值由集合上的倍数以某种恒定的方式确定空间。如果有帮助,很高兴扩展为其他答案。
    • @ScottChristopher:传感器总是让我头疼!我很想看看你的方法。虽然我们总是可以reducescanmapAccum 使用类似(({a, n}, b) => ({a: ((a * n) + b) / (n + 1), n: n + 1}))(({t, a, n}, b) => ({t: t + b, a: (t + b) / (n + 1), n: n + 1})) 的适当初始值,然后在最后提取a,但我不知道如何将其变为一个合理的换能器。
    • @ScottChristopher 我也对你的建议感兴趣。
    • 我在这里为一种这样的方法提供了答案。 @ScottSauyet - 这几乎就是我所采取的方法。转换器的使用特别值得商榷,尽管可能有助于避免对大型列表进行多次遍历。同样也可以在不创建新变压器的情况下使用累加器功能(如您共享的那个)来实现 - 唯一不错的额外功能是变压器让您可以在 @@transducer/step 中烘焙 sum/count 而不必这样做来自transduce 的值。我也分享你的换能器引起的头痛:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-22
    • 1970-01-01
    • 2017-05-03
    • 1970-01-01
    • 2017-04-06
    相关资源
    最近更新 更多