【问题标题】:Use ramdajs for iterative calculations使用 ramdajs 进行迭代计算
【发布时间】:2019-02-12 18:03:14
【问题描述】:

我正试图将我的头脑集中在 Ramda 和函数式编程上,看看它在我的情况下是否有意义。

下面是我需要解决的典型问题:

以下作为输入数据:

const values = [
 { a: 1,   b: 2,   c: 3 },
 { a: 10,  b: 20,  c: 30 },
 ...
 { a: 100, b: 200, c: 300 }
]

以下函数应用于数据:

const eFn = x => x.a + x.b
const fFn = ? // cumulative add: Sum of the e keys from first to current index, first iteration f = 3, then 3 + 30, then 3 + 30 +300
const gFN = x => x.f > x.e

在这个给定的序列中:

  1. eFn()
  2. fFn()
  3. gFn()

结果如下:

const results = [
 { a: 1,   b: 2,   c: 3,   e: 3,   f: 3,  g: true },
 { a: 10,  b: 20,  c: 30,  e: 30,  f: 33, g: true  },
 ...
 { a: 100, b: 200, c: 300, e: 300, f: 333, g: false }
]

问题:

对于这类问题,

  • 使用 ramda 有意义吗?
  • 它是否简化了问题,并且知道 fFn 依赖于前几行的数据并且必须在 fFn 之后应用 gFn 时,我是否可以避免在数据上循环多次?

我发现使用 Ramda 很难很好地解决这个问题。

我们将不胜感激。


更新 (2019-02-12)

基于@scott-sauyet 的回答,我确实尝试对 Ramda 与 Rambda 进行基准测试。 由于我无法 100% 复制他的测试,因此我对其进行了修改以更改 fFn 的行为并手动设置应用每个函数的次数。

    const {floor, random} = Math

    const demo = counts => {

      const eFns = R.curry((n, x) => R.assoc(`e${n}`, x.a + x.b, x))
      const fFns = R.curry((n, x) => R.assoc(`f${n}`,  x.d * x.b, x))
      const gFns = R.curry((n, x) => R.assoc(`g${n}`,  x.f > x.e, x))

      const transform = R.pipe(
        R.map(eFns(1)),
        R.map(eFns(2)),
        R.map(eFns(3)),
        R.map(eFns(4)),
        R.map(eFns(5)),
        R.map(eFns(6)),
        R.map(eFns(7)),
        R.map(eFns(8)),
        R.map(eFns(9)),
        R.map(eFns(10)),
        R.map(eFns(12)),
        R.map(eFns(13)),
        R.map(eFns(14)),
        R.map(eFns(15)),
        R.map(eFns(16)),
        R.map(eFns(17)),
        R.map(eFns(18)),
        R.map(eFns(19)),
        R.map(eFns(20)),
        R.map(eFns(21)),
        R.map(eFns(22)),
        R.map(eFns(23)),
        R.map(eFns(24)),
        R.map(eFns(25)),
        R.map(eFns(26)),
        R.map(eFns(27)),
        R.map(eFns(28)),
        R.map(eFns(29)),
        R.map(eFns(30)),
        R.map(eFns(31)),
        R.map(eFns(32)),
        R.map(eFns(33)),
        R.map(eFns(34)),
        R.map(eFns(35)),
        R.map(eFns(36)),
        R.map(eFns(37)),
        R.map(eFns(38)),
        R.map(eFns(39)),
        R.map(eFns(40)),
        R.map(fFns(1)),
        R.map(fFns(2)),
        R.map(fFns(3)),
        R.map(fFns(4)),
        R.map(fFns(5)),
        R.map(fFns(6)),
        R.map(fFns(7)),
        R.map(fFns(8)),
        R.map(fFns(9)),
        R.map(fFns(10)),
        R.map(gFns(1)),
        R.map(gFns(2)),
        R.map(gFns(3)),
        R.map(gFns(4)),
        R.map(gFns(5)),
        R.map(gFns(6)),
        R.map(gFns(7)),
        R.map(gFns(8)),
        R.map(gFns(9)),
        R.map(gFns(10))
      )


      const vals = R.times(n => ({
        a: floor(random() * 1000),
        b: floor(random() * 1000),
        c: floor(random() * 1000),
        d: floor(random() * 1000)
      }), counts)

      const now = new Date()
      transform(vals)
      const time = new Date() - now

      console.log(`Ran ${counts} records through ${eFns.length} e's, ${fFns.length} f's, and ${gFns.length} g's in ${time} ms`)
    }

    console.clear()
    demo(10)
    demo(100)
    demo(1000)
    demo(10000)
    demo(100000)

现在,我将此代码依次粘贴到 Ramda REPLRambda REPL。我在 Windows 7、带有 Chrome 66 的核心 i7-6820HQ 和 Node.js v8.11.1 上运行了测试。

令我惊讶的是,就我而言,Rambada 比 Ramda 慢。请注意,这是一个快速而肮脏的测试,我可能错过了为 Rambda 设置测试的正确方法(我只是想复制并粘贴每个 REPL 中的代码并通过修改 import 语句在节点中运行)。

这是我的结果: (注意图表是对数对数比例)

Record Number             [-] :  10 | 10 | 1000 | 10000  | 10000
Ramda Chrome 66  [time in ms] :  5  | 39 | 329  | 3673   | 38910  
Rambda Chrome 66 [time in ms] :  6  | 85 | 530  | 5306   | 53777  
Ramda Node.js    [time in ms] :  8  | 38 | 396  | 4219   | 45621
Rambda Node.js   [time in ms] :  7  | 62 | 537  | 5468   | 57540

【问题讨论】:

    标签: javascript ramda.js


    【解决方案1】:

    使用 Ramda 可能是有意义的。请注意,Ramda 的最佳点是一系列简单的转换 pipe'd 或 compose'd 一起创建一个更复杂的转换。因此,使用 Ramda 最直接的方法并不能满足您只循环一次的目标。在某些情况下,转换器可能会对此有所帮助,但许多 Ramda 函数还没有准备好转换器,所以你必须看看什么会起作用。

    但我认为,编写此类问题的正确方法是从简单的代码开始,并且仅在您确实遇到性能问题时才修复它们。编写简单的代码,如果它成为您应用程序中的瓶颈,则解决它。 (并且只有在您解决了任何更严重的瓶颈之后才这样做。)令人惊讶的是,您认为会成为问题的代码通常根本不是问题。

    所以我可能会这样处理这个问题:

    const {assoc, curry, tail, scan, pipe, map} = R
    
    const eFn = x => assoc('e', x.a + x.b, x)
    const fFn = (a, x) => assoc('f', a.f + x.e, x)
    const gFn = x => assoc('g', x.f > x.e, x)
    
    // scan includes initial value -- should this be fixed?
    const myScan = curry((fn, init, xs) => tail(scan(fn, init, xs)))
    
    const transform = pipe(
      map(eFn),
      myScan(fFn, {f: 0}),
      map(gFn)  
    )
    
    const values = [
     { a: 1,   b: 2,   c: 3 },
     { a: 10,  b: 20,  c: 30 },
     { a: 100, b: 200, c: 300 }
    ]
    
    console.log(transform(values))
    <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

    这表明scan 中存在一些缺陷(类似于mapAccum,但界面更简单)。 scan(add, 0, [1, 2, 3, 4]) //=> [0, 1, 3, 6, 10]。我看不出结果应该包括最初的0 的充分理由,尽管我没有去检查历史以查看我是否遗漏了一些重要的东西。我通过将它包装在一个由tail 跟随它的函数中来解决这个问题。但我们也可以轻松地将tail 添加到管道中:

    const transform = pipe(
      map(eFn),
      scan(fFn, {f: 0}), 
      tail,
      map(gFn)  
    )
    

    更新

    关于性能的评论。这是一个针对给定数量的记录测试许多 efg 样式函数的版本:

    const {curry, tail, scan, map, assoc, range, apply, pipe, addIndex, times} = R
    const {floor, random} = Math
    const myScan = curry((fn, init, xs) => tail(scan(fn, init, xs)))
    
    const demo = (es, fs, gs, counts) => {
    
      const eFns = map(n => (x) => assoc(`e${n}`, x.a + x.b, x), range(1, es + 1))
      const fFns = map(n => (a, x) => assoc(`f${n}`, a[`f${n}`] + x[`e${n}`], x), range(1, fs + 1))
      const gFns = map(n => (x) => assoc(`g${n}`, x[`f${n}`] > x[`e${n}`], x), range(1, gs + 1))
    
      const transform = apply(pipe)([...map(map, eFns), ...addIndex(map)((f, i) => myScan(f, {[`f${i + 1}`]: 0}), fFns), ...map(map, gFns)])
    
      const vals = times(n => ({
        a: floor(random() * 1000),
        b: floor(random() * 1000),
        c: floor(random() * 1000),
      }), counts)
    
      const now = new Date()
      transform(vals)
      const time = new Date() - now
    
      console.log(`Ran ${counts} records through ${eFns.length} e's, ${fFns.length} f's, and ${gFns.length} g's in ${time} ms`)
    }
    
    console.clear()
    demo(40, 10, 10, 100)
    demo(40, 10, 10, 1000)
    demo(40, 10, 10, 10000)
    <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

    对于具有 40 个 e、10 个 f 和 10 个 g 的 10000 条记录,我在新 MacBook Pro 上的 Chrome 中获得大约 2.5 秒的时间。我不知道这对您的申请是否合理。 (你也可以在 Ramda REPL 上玩这个。)

    【讨论】:

    • 感谢您的回答。即使它涉及更多的循环(每次都循环良好),这也是非常可读的。我必须根据性能来评估成本,就像在我的应用程序中一样,有 50 个函数,如 eFn,可能有 10 个函数,如 fFn,这将需要在 10000 个元素的数组上进行尽可能多的循环。也许性能会是可以接受的。我还看到像 Rambda 这样的库应该胜过 ramda。
    • 我没怎么关注Rambda,但我知道这是他们的主张。这很可能是真的,因为他们试图做的更少。我已经更新以包含一些性能数字。对于 10000 条随机记录,有 40 个 eFns、10 个 fFns 和 10 个 gFns,我得到的时间约为 2.5 秒。
    • 感谢您的更新。我将调整你的代码,看看 Rambda.js 在这种情况下与 Ramda 相比如何执行。我将在本周发布此作为新回复。虽然从技术上讲,@customcommander 提出的答案避免了对输入数据的额外循环,但我发现当我的情况需要应用多个函数时,您的解决方案更具可读性。感谢你们两位的投入。
    【解决方案2】:

    我认为使用 Ramda 是有意义的:

    1. Ramda 不会改变数据
    2. Ramda 确实有很多函数可以帮助您处理列表和对象

    这会改变原始对象:

    const add_ab = obj => { obj.e = obj.a + obj.b; return obj };
    

    而这不会:

    const add_ab = obj => assoc('e', obj.a + obj.b, obj);
    

    您可以将迭代累加值与mapAccum结合起来,这可能对您的情况有用:

    mapAccum 函数的行为类似于 map 和 reduce 的组合;它将一个函数应用于列表的每个元素,从左到右传递一个累加参数,并将这个累加器的最终值与新列表一起返回。

    迭代器函数接收两个参数,acc 和 value,并且应该返回一个元组 [acc, value]。

    const {assoc, compose, last, mapAccum} = R;
    
    const data = [
     { a: 1,   b: 2,   c: 3 },
     { a: 10,  b: 20,  c: 30 },
     { a: 100, b: 200, c: 300 }
    ];
    
    
    const set_e = obj => assoc('e', obj.a + obj.b, obj);
    const set_f = (acc, obj) =>  assoc('f', acc.f + obj.e, obj);
    const set_g = obj => assoc('g', obj.f > obj.e, obj);
    
    const execute = compose(last, mapAccum((acc, cur) => [
      set_f(acc, set_e(cur)),
      set_g(set_f(acc, set_e(cur)))
    ], {f: 0}));
    
    console.log(
     execute(data)
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>

    【讨论】:

    • 感谢您的回答。有没有办法使用R.composeR.pipe 来提高set_g(set_f(acc, set_e(cur))) 的可读性?在实际情况下,我必须在 set_f 之前和之后应用更多功能。另外,有没有办法避免多次调用 set_f ?
    • 谢谢。不幸的是 set_f 必须依赖于之前的迭代。
    • 实际上代码并没有按预期工作:最后一次迭代给出 330 但应该是 333。注意:我已经更新了我的示例以反映您的代码,因为我最初的帖子中有一个类型。跨度>
    • 实际上,您应该将 set_f 从const set_f = (acc, obj) => assoc('f', acc.e + obj.e, obj); 更改为const set_f = (acc, obj) => assoc('f', acc.f + obj.e, obj);,并将初始值从{ e: 0 } 更改为{f: 0}。也许我的措辞对解释 set_f 有误导性,我已经更新了我的初始帖子。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-05
    • 1970-01-01
    • 2012-01-31
    • 2018-09-19
    • 1970-01-01
    相关资源
    最近更新 更多