Круглый функциональный подход - почему у моей функции есть побочные эффекты?

Цель

Я пытаюсь создать алгоритм Round Robin (https://en.wikipedia.org/wiki/Round-robin_scheduling) в чисто функциональном ключе.

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

[
    [ 1, 2 ],
    [ 3, 4 ]
]

И произведите следующий вывод:

[ 1, 3, 2, 4 ]

код

Чтобы достичь этого, я решил рекурсивно реализовать циклический цикл, как показано ниже:

const roundRobin = (arr, results) => {
    if (arr.length === 0) return results;

    const newResults = arr.reduce((acc, current) => {

        if (current.length > 0) {
            acc.results.push(current.shift());
            acc.arr.push(current);
        }
        return acc;

    }, { arr: [], results });

    return roundRobin(newResults.arr, newResults.results);
};

Здесь я чувствую массу результатов, и я заканчиваю, когда мне нечего добавить к нему. Использовать этот код можно следующим образом:

const array =     [
        [ 1, 2 ],
        [ 3, 4 ]
    ];

const result = roundRobin( array, [] );

Проблема

В моем коде я использую reduce в моем параметре arr, чтобы убедиться, что я не изменяю оригинал. Однако, если я печатаю массив перед использованием roundRobin и после, переменная изменяется! Я как-то мутирую его!

Вопросы:

  • Если я использую сокращение, которое является чистым, как я изменяю свои параметры?
  • Есть ли еще один чистый/функциональный способ реализации roundRobin?

Ответ 1

  • Если я использую сокращение, которое является чистым, как я изменяю свои параметры?

Параметры функции не могут быть мутированы; странная мысль - но я уверен, что вы имели в виду, что аргументы, предоставленные вашей функции, мутируются. И да, это с .shift, как указывали другие.

И для того, что стоит, .reduce не является чистым, если только предоставленная пользователем лямбда не является чистой

  1. Есть ли еще один чистый/функциональный способ реализации roundRobin?

Да

const isEmpty = xs =>
  xs.length === 0
  
const head = ( [ x , ...xs ] ) =>
  x
  
const tail = ( [ x , ...xs ] ) =>
  xs

const append = ( xs , x ) =>
  xs.concat ( [ x ] )
  
const roundRobin = ( [ x , ...xs ] , acc = [] ) =>
  x === undefined
    ? acc
    : isEmpty ( x )
      ? roundRobin ( xs , acc )
      : roundRobin ( append ( xs , tail ( x ) )
                   , append ( acc , head ( x ) )
                   )

const data =
  [ [ 1 , 4 , 7 , 9 ]
  , [ 2 , 5 ]
  , [ 3 , 6 , 8 , 10 , 11 , 12 ]
  ]
                   
console.log ( roundRobin ( data ) )
// => [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ]

console.log ( roundRobin ( [ [ 1 , 2 , 3 ] ] ) )
// => [ 1 , 2 , 3 ]

console.log ( roundRobin ( [] ) )
// => []

Ответ 2

Array # shift выполняет мутацию.

var array = [0, 1, 2, 3, 4];
array.shift(); // -> 0
array; // -> [1, 2, 3, 4];

Самый простой способ - клонировать массив. Обычно это можно сделать с помощью Array # concat, но поскольку ваши массивы вложены (хотя и простые), вы можете сделать это:

const roundRobin = (arr, results) => {
    arr = JSON.parse(JSON.stringify(arr));
    if (arr.length === 0) return results;
    // ...

Если вы обеспокоены тем, что глобальный JSON делает функцию нечистой, вы можете абстрагировать ее.

const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
roundRobin(deepClone(array), []);

Ответ 3

Простым способом создания неизменяемого объекта в JS является использование Object.freeze.

Я создал ваш входной массив как:

const array1 = Object.freeze([
    Object.freeze([ 1, 2 ]),
    Object.freeze([ 3, 4 ])
])

Затем, когда я попытался вызвать вашу функцию, я получил:

Uncaught TypeError: Cannot add/remove sealed array elements
at Array.shift (<anonymous>)
at arr.reduce (<anonymous>:7:38)
at Array.reduce (<anonymous>)
at roundRobin (<anonymous>:4:28)
at <anonymous>:6:17

Используется shift, который мутирует массив orignal.

Замените его на Array.slice(0,1)[0] и он начнет работать.