繁体   English   中英

使用ramda.js将命令式转换为功能样式

[英]Convert imperative to functional style using ramda.js

我正在编写使用命令式样式将数字数组转换为新数据列表的代码,但我想使用ramdajs之类的JavaScript库将其转换为函数样式

代码背景假设美元价值总共有5个硬币,分别是25美元,20美元,... 1美元。 我们将不得不用钱兑换美元硬币。 用最少的硬币

const data = [25, 20, 10, 5, 1];
const fn = n => data.map((v) => {
    const numberOfCoin = Number.parseInt(n / v, 10);
    const result = [v, numberOfCoin];
    n %= v;
    return numberOfCoin ? result : [];
  }).filter(i => i.length > 0);

该代码的结果应为

fn(48) => [[25, 1], [20, 1], [1, 3]]
fn(100) => [[25, 4]]

我认为您已经有了一个不错的开始,但是为了使它更具功能性,我需要进行一些更改:

  1. 使用表达式而不是语句(例如,不return
  2. 不要变异数据(例如,否n %= v

您不必为此需要Ramda:

 const coins = value => [25, 20, 10, 5, 1].reduce(([acc, val], cur) => val < cur ? [acc, val] : [[...acc, [cur, Math.floor(val / cur)]], val % cur], [[], value] )[0]; console.log(coins(48)); console.log(coins(100)); 

如果发现自己使用map然后进行filter ,则很可能需要reduce 在上面的函数coins ,迭代器返回一个数组,该数组包含钱币对和钱币数量以及每个步骤的折减值的数组。

请注意,在每个步骤中,我都使用解构分配来捕获对的数组以及各个参数中的减小值。

现在,当然也可以使用Ramda了:

 const {compose, filter, last, mapAccum, flip} = R; const mapIterator = (a, b) => [a % b, [b, Math.floor(a / b)]]; const withCoins = ([coins, number]) => number > 0; const coins = compose(filter(withCoins), last, flip(mapAccum(mapIterator))([25, 20, 10, 5, 1])); console.log(coins(48)); console.log(coins(100)); 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script> 

编辑 :正如Scott正确指出的那样,我上面的任何解决方案都将为您带来最少的更改。

事实证明,这比我预期的要复杂得多,我确定了一个可以肯定的解决方案:

我定义了5套硬币:

  1. [1]
  2. [5,1]
  3. [10、5、1]
  4. [20、10、5、1]
  5. [25、20、10、5、1]

我计算每组产生多少变化,并仅保留产生最少变化的变化。

例如更改30

  1. 1×30
  2. 5×6
  3. 10×3
  4. 20×1、10×1 (保留此设置)
  5. 25×1、5×1

 const {compose, pipe, sum, map, last, head, mapAccum, curry, flip, applyTo, sortBy, reject, not} = R; const numCoins = compose(sum, map(last)); const changeFn = curry((coins, num) => mapAccum((cur, coin) => [cur % coin, [coin, Math.floor(cur / coin)]], num, coins)[1]); const change1 = changeFn([1]); const change2 = changeFn([5, 1]); const change3 = changeFn([10, 5, 1]); const change4 = changeFn([20, 10, 5, 1]); const change5 = changeFn([25, 20, 10, 5, 1]); const change = pipe( applyTo, flip(map)([ change1, change2, change3, change4, change5]), sortBy(numCoins), head, reject(compose(not, last))); console.log(change(30)); console.log(change(40)); console.log(change(48)); 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script> 

user633183的答案的这种变体将找到最小数量的硬币,它没有使用选择每个较大面额的最大数量的通用技术,而这可能会牺牲总体上选择更多硬币的可能性。

请注意,与原始答案或来自customcommander的初始答案相比,这可能涉及更多的计算。

这里的change返回硬币值的列表,因此对于58 ,它将返回[25, 25, 5, 1, 1, 1] makeChange将其转换为[ [25, 2], [5, 1], [1, 3] ] makeChange [ [25, 2], [5, 1], [1, 3] ]格式。 如果将minLength函数从<=更改为< ,则将生成[ [25, 1], [20, 1], [10, 1], [1, 3] ] 这是相同数量的硬币,但使用不同的面额。

如果退货顺序对您来说无关紧要,您也可以删除sort行。

这里的样式混合有点不幸。 我们可以更换的Ramda管道版本makeChange一个更像change ,如果我们的努力。 但是我认为在拉姆达 这是最容易想到的。 用Ramda管道替换change并不容易,因为以这种方式进行递归比较困难。


感谢 customcommander指出此答案的早期版本中的一个缺陷。


 const minLength = (as, bs) => as.length <= bs.length ? as : bs const change = ([ c, ...rest ], amount = 0) => amount === 0 ? [] : c === 1 ? Array(amount).fill(1) : c <= amount ? minLength ( [ c, ...change ([c, ...rest], amount - c)], change (rest, amount) ) : change (rest, amount) const makeChange = pipe( change, countBy(identity), toPairs, map(map(Number)), sort(descend(head)) // if desired ) const coins = [ 25, 20, 10, 5, 1 ] console.log (makeChange (coins, 40)) //=> [ [ 20, 2 ] ] console.log (makeChange (coins, 45)) //=> [ [ 25, 1 ], [ 20, 1 ] ] console.log (change (coins, 48)) //=> [ [ 25, 1 ], [ 20, 1 ], [ 1, 3 ] ] console.log (makeChange (coins, 100)) //=> [ [ 25, 4 ] ] 
 <script src = "https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script> <script> const { pipe, countBy, identity, toPairs, map, sort, descend, head } = R </script> 

人们通常会伸手去拿mapfilterreduce但结果通常是在圆孔中有点方形钉。

  • map没有意义,因为它会产生一对一的结果; 如果我有4种硬币,我将总是收到4种找零,这当然不是我们想要的。 使用filter迫使您进行更多处理以获得所需的结果。
  • reduce可以消除由map + filter引起的中间值,但是同样, 分析每个硬币之前 ,我们有可能达到期望的结果。 在的示例fn(100)其返回[ [25, 4] ]就没有必要对连看硬币20105 ,或1因为结果已经达到; 进一步减少将是浪费。

对我来说,函数式编程是为了方便。 如果我没有满足我需要的功能,那么我就简单地做到了,因为重要的是我的程序必须清楚地传达其意图。 有时,这意味着使用更适合我正在处理的数据的构造-

const change = (coins = [], amount = 0) =>

  loop                             // begin a loop, initializing:
    ( ( acc = []                   // an empty accumulator, acc
      , r = amount                 // the remaining amount to make change for, r
      , [ c, ...rest ] = coins     // the first coin, c, and the rest of coins
      ) =>

        r === 0                    // if the remainder is zero
          ? acc                    // return the accumulator

      : c <= r                     // if the coin is small enough
          ? recur                              // recur with
              ( [ ...acc, [ c, div (r, c) ] ]  // updated acc
              , mod (r, c)                     // updated remainder
              , rest                           // rest of coins
              )

                                   // otherwise (inductive) coin is too large
      : recur                      // recur with
          ( acc                    // unmodified acc
          , r                      // unmodified remainder
          , rest                   // rest of coins
          )

    )

mapfilterreduce ,我们的解决方案在确定结果之后将不会继续对输入进行迭代。 使用它看起来像这样-

const coins =
  [ 25, 20, 10, 5, 1 ]

console.log (change (coins, 48))
// [ [ 25, 1 ], [ 20, 1 ], [ 1, 3 ] ]

console.log (change (coins, 100))
// [ [ 25, 4 ] ]

在下面的浏览器中验证结果-

 const div = (x, y) => Math .round (x / y) const mod = (x, y) => x % y const recur = (...values) => ({ recur, values }) const loop = f => { let acc = f () while (acc && acc.recur === recur) acc = f (...acc.values) return acc } const change = (coins = [], amount = 0) => loop ( ( acc = [] , r = amount , [ c, ...rest ] = coins ) => r === 0 ? acc : c <= r ? recur ( [ ...acc, [ c, div (r, c) ] ] , mod (r, c) , rest ) : recur ( acc , r , rest ) ) const coins = [ 25, 20, 10, 5, 1 ] console.log (change (coins, 48)) // [ [ 25, 1 ], [ 20, 1 ], [ 1, 3 ] ] console.log (change (coins, 100)) // [ [ 25, 4 ] ] 

R.until用户可以使用R.until ,但由于它是纯函数驱动的界面,因此可读性受到影响。 IMO非常喜欢looprecur灵活性-

const change = (coins = [], amount = 0) =>
  R.until
    ( ([ acc, r, coins ]) => r === 0
    , ([ acc, r, [ c, ...rest ] ]) =>
        c <= r
          ? [ [ ...acc
              , [ c, Math.floor (R.divide (r, c)) ]
              ]
            ,  R.modulo (r, c)
            , rest
            ]
          : [ acc
            , r
            , rest
            ]
    , [ [], amount, coins ]
    )
    [ 0 ]

另一种选择是将其编写为递归函数-

 const div = (x, y) => Math .round (x / y) const mod = (x, y) => x % y const change = ([ c, ...rest ], amount = 0) => amount === 0 ? [] : c <= amount ? [ [ c, div (amount, c) ] , ...change (rest, mod (amount, c)) ] : change (rest, amount) const coins = [ 25, 20, 10, 5, 1 ] console.log (change (coins, 48)) // [ [ 25, 1 ], [ 20, 1 ], [ 1, 3 ] ] console.log (change (coins, 100)) // [ [ 25, 4 ] ] 

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM