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

时间:2019-02-19 06:05:55

标签: javascript functional-programming 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]]

3 个答案:

答案 0 :(得分:2)

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

  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>

答案 1 :(得分:2)

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

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

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

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

这里的样式混合有点不幸。如果我们尝试过的话,可以用makeChange之类的其他change替代Ramda管道版本。但是我认为在拉姆达这是最容易想到的。用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>

答案 2 :(得分:1)

人们通常会碰到mapfilterreduce,但通常结果是在圆孔中有点方形钉。

  • map毫无意义,因为它会产生一对一的结果。如果我有4种硬币,我将总是收到4种找零,这当然不是我们想要的。使用filter会迫使您进行更多处理以获得所需的结果。
  • reduce可以消除由map + filter引起的中间值,但是同样,我们有可能在必须分析每个硬币之前达到期望的结果。在返回fn(100)的{​​{1}}的示例中,甚至不需要查看硬币[ [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 ) ) mapfilter,我们的解决方案将在确定结果之后继续对输入进行迭代。使用它看起来像这样-

reduce

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

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用户可以使用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.untilloop的灵活性非常有利,imo-

recur

另一种替代方法是将其编写为递归函数-

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 ]