【问题标题】:Convert imperative to functional style using ramda.js使用 ramda.js 将命令式转换为函数式
【发布时间】:2019-02-19 06:05:54
【问题描述】:

我正在编写代码以使用命令式样式将数字数组转换为新数据列表,但我想使用 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]]

【问题讨论】:

  • 你能解释一下算法——输入与输出的关系如何?此外,您提供的代码已经可以正常工作,并不是必须的。
  • @VLAZ 抱歉,我忘了解释这件事的背景。我刚刚编辑了这篇文章。
  • 请注意,此代码和@customcommander 的答案都没有提供最少数量的硬币。 (40 = 25 + 10 + 5 对 40 = 20 + 20)。它仍然是一种有用的算法,并且经常进行更改,但与描述不符。
  • 你是对的。恐怕我完全忽略了这一点。谢谢你。
  • @aloebys 你能澄清一下斯科特的评论吗?

标签: javascript functional-programming ramda.js


【解决方案1】:

我认为您已经有了一个不错的开始,但我会更改一些内容以使其更实用:

  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));
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"&gt;&lt;/script&gt;

编辑:正如 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));
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"&gt;&lt;/script&gt;

【讨论】:

  • 我发现 ramda 版本更具可读性,但 YMMV。一个挑剔:虽然reduce肯定比映射后过滤更快,但后者以组合reduce所没有的方式传达代码的意图(关注点分离)。 Compose 让您拥有自己的蛋糕并吃掉它。
  • +1 我觉得让 OP 知道他们在最初的尝试中并没有那么遥远是很重要的。话虽如此,我当然承认非 Ramda 功能感觉更复杂。就我个人而言,我也会使用 Ramda 并编写函数。
  • 我认为这些生成案例40 = 20 + 20 的更改可能涉及必要的计算。很可能有更优雅的递归,但这不会改变执行的计算次数。
【解决方案2】:

来自 user633183 的答案的这个变体将找到最小数量的硬币,它不使用通常的技术,即选择每个较大面额的最大数量,可能会牺牲整体选择更多硬币。

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

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

如果您不关心退货的顺序,您也可以删除sort 这一行。

这里的风格混合有点不幸。如果我们尝试的话,我们可以将 makeChange 的 Ramda 管道版本替换为更多类似 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>

【讨论】:

    【解决方案3】:

    人们通常会伸手去拿mapfilterreduce,但结果通常是圆孔中的方钉。

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

    对我来说,函数式编程是为了方便。如果我没有一个功能可以满足我的需求,我只需制作它,因为我的程序清楚地传达其意图很重要。有时这意味着使用更适合我正在处理的数据的构造 -

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

    Ramda 用户可以使用R.until,但由于它是纯函数驱动的界面,可读性会受到影响。 looprecur 的灵活性是非常有利的,imo -

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

    【讨论】:

    • 我认为这里的递归部分是最​​干净的答案......但前提是目标是 40 = 25 + 10 + 5 而不是我上面提到的 40 = 20 + 20。如果是后者,那么我认为需要一种完全不同的方法。
    • 感谢您的反馈,斯科特。我对答案的其他部分并不完全满意,但改进它们的成本超过了我认为改进会产生的额外影响。 40 = 20 + 20 确实要困难得多。如果那是 OP 需要的,我很乐意扩展答案。
    猜你喜欢
    • 2017-04-18
    • 2011-09-30
    • 1970-01-01
    • 2016-01-08
    • 2012-10-20
    • 1970-01-01
    • 2013-10-11
    • 2012-07-02
    • 2012-09-11
    相关资源
    最近更新 更多