简体   繁体   English

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

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

I'm write the code for convert array of number to new datalist using imperative style but i want to convert it to functional style using javascript library like ramdajs 我正在编写使用命令式样式将数字数组转换为新数据列表的代码,但我想使用ramdajs之类的JavaScript库将其转换为函数样式

background of code Suppose the dollar value There are 5 coins in total, 25 dollars, 20 dollars, ... 1 dollar. 代码背景假设美元价值总共有5个硬币,分别是25美元,20美元,... 1美元。 We will have to exchange money for dollar coins. 我们将不得不用钱兑换美元硬币。 With the least amount of coins 用最少的硬币

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

the result of this code should be 该代码的结果应为

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

I think you had a pretty good start already but there are a few things I'd change in order to make it more functional: 我认为您已经有了一个不错的开始,但是为了使它更具功能性,我需要进行一些更改:

  1. Use expressions rather than statements (eg no return ) 使用表达式而不是语句(例如,不return
  2. Do not mutate data (eg no n %= v ) 不要变异数据(例如,否n %= v

You don't necessarily need Ramda for this: 您不必为此需要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)); 

If you find yourself using map then filter , you're most likely needing reduce . 如果发现自己使用map然后进行filter ,则很可能需要reduce In my function coins above, the iterator returns an array that contains an array of pairs of coins and number of coins and the reduced value for each step. 在上面的函数coins ,迭代器返回一个数组,该数组包含钱币对和钱币数量以及每个步骤的折减值的数组。

Note that at each step I use a destructuring assignment to capture the array of pairs and the reduced value in individual parameters. 请注意,在每个步骤中,我都使用解构分配来捕获对的数组以及各个参数中的减小值。

Now, it is of course possible to use Ramda for this as well: 现在,当然也可以使用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> 

EDIT : as Scott rightly pointed out, any of my solutions above will give you the least amount of change. 编辑 :正如Scott正确指出的那样,我上面的任何解决方案都将为您带来最少的更改。

This turned out to be more involved than I expected and I settled on a solution which I'm sure can be improved: 事实证明,这比我预期的要复杂得多,我确定了一个可以肯定的解决方案:

I define 5 sets of coins: 我定义了5套硬币:

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

I compute how much change each set produces and keep only the one which produces the least. 我计算每组产生多少变化,并仅保留产生最少变化的变化。

For example to change 30 : 例如更改30

  1. 1 × 30 1×30
  2. 5 × 6 5×6
  3. 10 × 3 10×3
  4. 20 × 1, 10 × 1 (Keep this set) 20×1、10×1 (保留此设置)
  5. 25 × 1, 5 × 1 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> 

This variant of the answer from user633183 will find the minimum number of coins, it doesn't use the common technique that chooses the maximum number of each larger denomination at the possible expense of choosing more coins overall. user633183的答案的这种变体将找到最小数量的硬币,它没有使用选择每个较大面额的最大数量的通用技术,而这可能会牺牲总体上选择更多硬币的可能性。

Note that this can involve substantially more calculations than the original answers or the initial ones from customcommander. 请注意,与原始答案或来自customcommander的初始答案相比,这可能涉及更多的计算。

change here returns a list of coin values, so for, say, 58 it will return [25, 25, 5, 1, 1, 1] . 这里的change返回硬币值的列表,因此对于58 ,它将返回[25, 25, 5, 1, 1, 1] makeChange converts that to the [ [25, 2], [5, 1], [1, 3] ] format. makeChange将其转换为[ [25, 2], [5, 1], [1, 3] ] makeChange [ [25, 2], [5, 1], [1, 3] ]格式。 If you change the minLength function from <= to < , then this would generate [ [25, 1], [20, 1], [10, 1], [1, 3] ] . 如果将minLength函数从<=更改为< ,则将生成[ [25, 1], [20, 1], [10, 1], [1, 3] ] This is the same number of coins, but using different denominations. 这是相同数量的硬币,但使用不同的面额。

If the order of the return doesn't matter to you, you could also remove the sort line. 如果退货顺序对您来说无关紧要,您也可以删除sort行。

The mix of styles here is somewhat unfortunate. 这里的样式混合有点不幸。 We could replace that Ramda pipeline version of makeChange with one more like change if we tried. 我们可以更换的Ramda管道版本makeChange一个更像change ,如果我们的努力。 But I think in Ramda; 但是我认为在拉姆达 this is what came most easily to mind. 这是最容易想到的。 Replacing change with a Ramda pipeline would not be as easy, as it's harder to do recursion in such a style. 用Ramda管道替换change并不容易,因为以这种方式进行递归比较困难。


Thanks to customcommander for pointing out a flaw in an earlier version of this answer. 感谢 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> 

People commonly reach for map , filter , and reduce but often the result is a bit of a square peg in a round hole. 人们通常会伸手去拿mapfilterreduce但结果通常是在圆孔中有点方形钉。

  • map doesn't makes sense because it's produces a 1-to-1 result; map没有意义,因为它会产生一对一的结果; if I have 4 types of coins, I will always receive 4 types of change, which of course is not what we want. 如果我有4种硬币,我将总是收到4种找零,这当然不是我们想要的。 Use of filter forces you to do more processing to achieve the desired result. 使用filter迫使您进行更多处理以获得所需的结果。
  • reduce can eliminate intermediate values caused by map + filter , but again, it's possible we will reach the desired result before having to analyze each coin. reduce可以消除由map + filter引起的中间值,但是同样, 分析每个硬币之前 ,我们有可能达到期望的结果。 In the example of fn(100) which returns [ [25, 4] ] , there's no need to even look at coins 20 , 10 , 5 , or 1 because the result has already been reached; 在的示例fn(100)其返回[ [25, 4] ]就没有必要对连看硬币20105 ,或1因为结果已经达到; reducing further would be wasteful. 进一步减少将是浪费。

To me, functional programming is about convenience. 对我来说,函数式编程是为了方便。 If I don't have a function that does what I need, I simply make it, because it's important that my program clearly communicates its intention. 如果我没有满足我需要的功能,那么我就简单地做到了,因为重要的是我的程序必须清楚地传达其意图。 Sometimes this means using a construct that's more suitable for datas I'm processing - 有时,这意味着使用更适合我正在处理的数据的构造-

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
          )

    )

Unlike map , filter , and reduce , our solution will not continue iterating over the input after the result has been determined. mapfilterreduce ,我们的解决方案在确定结果之后将不会继续对输入进行迭代。 Using it looks like this - 使用它看起来像这样-

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

Verify the results in your own browser below - 在下面的浏览器中验证结果-

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

Ramda users can use R.until , though readability suffers due to it's purely function-driven interface. R.until用户可以使用R.until ,但由于它是纯函数驱动的界面,因此可读性受到影响。 Flexibility of loop and recur is highly favourable, imo - 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 ]

Another alternative is to write it as a recursive function - 另一种选择是将其编写为递归函数-

 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