Преобразуйте императив в функциональный стиль, используя ramda.js - PullRequest
0 голосов
/ 19 февраля 2019

Я пишу код для преобразования массива чисел в новый список данных, используя императивный стиль, но я хочу преобразовать его в функциональный стиль, используя библиотеку javascript, например ramdajs

фон кода Предположимдолларовая стоимость Всего 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 голосов
/ 19 февраля 2019

Люди обычно достигают значений map, filter и reduce, но часто результатом является небольшой квадратный колышек в круглой дыре.

  • map не имеет смысла, поскольку дает результат 1 к 1;если у меня есть 4 типа монет, я всегда получу 4 типа изменений, что, конечно, не то, что мы хотим.Использование filter заставляет вас выполнять больше обработки для достижения желаемого результата.
  • reduce может устранить промежуточные значения, вызванные map + filter, но, опять же, возможно, мы достигнем желаемогорезультат до необходимости анализировать каждую монету.В примере fn(100), который возвращает [ [25, 4] ], нет необходимости даже смотреть на монеты 20, 10, 5 или 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
          )

    )

В отличие от map, filter и 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 ] ]

Проверьте результаты в вашем собственном браузере ниже -

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, хотя читаемость страдает из-за чисто функционального интерфейса.Гибкость loop и recur очень благоприятна, 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 ] ]
0 голосов
/ 20 февраля 2019

Этот вариант ответа от пользователя 633183 найдет минимальное количество монет, он не использует общую технику, которая выбирает максимальное количество каждого большего номинала, за счет возможного выбора большего количества монет в целом.

Обратите внимание, что для этого может потребоваться значительно больше вычислений, чем для исходных ответов или исходных ответов от 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.

Миксстилей здесь несколько неудачно.Мы могли бы заменить эту версию конвейера Ramda makeChange еще одной, например change, если бы мы попробовалиНо я думаю в Рамде;это то, что пришло в голову легче всего.Заменить change конвейером Ramda было бы не так просто, так как рекурсию в таком стиле сделать сложнее.


Спасибо 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>
0 голосов
/ 19 февраля 2019

Я думаю, что вы уже неплохо начали, но есть несколько вещей, которые я бы изменил, чтобы сделать его более функциональным:

  1. Используйте выражения вместо выражений (например, нет return)
  2. Не изменять данные (например, нет n %= v)

Рамда для этого необязательно:

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 выше, итератор возвращает массив, который содержит массив пар монет и количество монет и уменьшенное значение для каждого шага.

Обратите внимание, что на каждом шаге я использую присвоение деструктуры длязахватить массив пар и приведенное значение в отдельных параметрах.

Теперь, конечно, можно использовать и Рамду для этого:

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>

РЕДАКТИРОВАТЬ : как правильно заметил Скотт, любое из моих решений, приведенных выше, даст вам наименьшее количество изменений.

Это оказалось более сложным, чем я ожидал, и я остановился на решении, которое, я уверен, можно улучшить:

Я определяю 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>
...