Главная Категории Контакты Поиск

mutable и immutable структуры данных

Как избежать мутаций в JavaScript с помощью неизменных структур данных.

JavaScriptFunctional programming·23.05.2019·читать 4 мин 🤓·Автор: Alexey Myzgin

Как избежать мутаций в JavaScript с помощью неизменных структур данных.

Неизменные (immutable) данные необходимы в функциональном программировании, потому что мутации являются побочным эффектом. Наше преобразование данных не должно влиять на исходный источник данных, а должно возвращать новый источник данных с нашими обновлениями.

Чтобы продемонстрировать разницу между изменчивостью и неизменностью, представь, что ты пьешь воду из стакана. Если стакан изменчив, то когда ты пьешь, ты сохраняешь тот же стакан, но меняешь количество воды в нем. Однако, если стакан неизменчив, когда ты пьешь, то получаешь совершенно новый, идентичный стакан, содержащий правильное количество оставшейся воды. Возможно, это странный способ представить себе действие, но создание новых структур данных делает наши методы чистыми и поточно-ориентированными, что является преимуществом функционального программирования.

mutable структуры данных могут быть изменены после создания, а immutable - нет. Мутации могут рассматриваться как побочные эффекты в наших приложениях.

mutable и immutable данные в массиве

Давай создадим массив a с несколькими значениями. Мы присвоим переменную a к b. Что создаст новую переменную b с той же ссылкой на значения, что и a. Мы можем убедиться в этом, используя проверку со строгим равенством, поскольку она будет проверять, ссылки на значения.

const a = [1, 2, 3];
const b = a;
console.log(a === b); // true

Теперь, если мы обновим b, добавив в него значение, и выведем в консоль a, то увидим, что a также обновилось. Это потому, что a и b не являются разными массивами. Они являются ссылкой на тот же массив.

const a = [1, 2, 3];
const b = a;
b.push(4);
console.log(a); // [ 1, 2, 3, 4 ]

Когда мы вносим изменения в одну переменную, то, фактически, вносим изменения и в другую. Это может вызвать проблемы в коде. Функциональность, которая работает на b, изменит a, даже если мы не собирались этого делать. То же самое относится и к объектам.

Например, у нас есть объект a, со свойством name и значением Alex. Если мы присвоим переменную a к b и внесем изменения в b, переназначив name с Alex на Julia, и выведем в консоль a.name, то увидим, что оно обновилось.

const a = { name: "Alex" };
const b = a;
b.name = "Julia";
console.log(a.name); // Julia

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

Например, метод push - это мутация в массиве. Как мы видели, он изменяет все ссылки на тот же массив. Тем не менее, мы можем создать immutable функцию высшего порядка push. Она получит value, затем array, и вернет новый массив, используя оператор распространения spread для создания клона, а затем поместит значение в него.

Более подробнее о функциях высшего порядка.

const push = value => array => {
  const clone = [...array];
  clone.push(value);
  return clone;
};

Если мы снова создадим массив const a = [1, 2, 3], и создадим второй массив, поместив в него значение с помощью нашей новой функции - push(), то увидим, что a не был изменен. Мы также можем увидеть, что a и b не равны, так как они являются ссылками на разные массивы. В данном примере, при вызове push(4)(a) - value равен 4, а array равен массиву a.

const push = value => array => {
  const clone = [...array];
  clone.push(value);
  return clone;
};

const a = [1, 2, 3];
const b = push(4)(a);

console.log(a); // [1, 2, 3]
console.log(b); // [1, 2, 3, 4]
console.log(a === b); // false

mutable и immutable данные в объекте

Мы можем создать аналогичные функции для обработки изменений в объектах. Для этого создадим два класса MutableGlass и ImmutableGlass.

Мы создадим метод takeDrink для них обоих и посмотрим на разницу между mutable и immutable обработкой. Начнем с MutableGlass. У нас будет constructor, который принимает content, amount и присваивает this.content к content, а this.amount к amount.

class MutableGlass {
  constructor(content, amount) {
    this.content = content;
    this.amount = amount;
  }
}

Затем мы создадим метод takeDrink, который будет принимать value. Он будет обновлять this.amount напрямую и вернет this экземпляра класса. Мы используем Math.max(this.amount - value, 0) чтобы гарантировать, что мы никогда не получим значение меньше нуля в нашем случае.

class MutableGlass {
  constructor(content, amount) {
    this.content = content;
    this.amount = amount;
  }

  takeDrink(value) {
    this.amount = Math.max(this.amount - value, 0);
    return this;
  }
}

Мы создадим стакан mg1 через new MutableGlass и передадим ему в качестве content - 'water', а amount - 100.

class MutableGlass {
  constructor(content, amount) {
    this.content = content;
    this.amount = amount;
  }

  takeDrink(value) {
    this.amount = Math.max(this.amount - value, 0);
    return this;
  }
}

const mg1 = new MutableGlass("water", 100);

Теперь, если мы сравним значения, путем проверки со строгим равенством, после вызова takeDrink, то увидим, что они равны. Это имеет смысл, так как они ссылаются на один и тот же экземпляр.

const mg1 = new MutableGlass("water", 100);
const mg2 = mg1.takeDrink(20);
console.log(mg1 === mg2); // true
console.log(mg1.amount === mg2.amount); // true

Это происходит потому, что мы изменяем значение напрямую, меняя структуру данных после её создания.

Теперь мы создадим ImmutableGlass. constructor будет выглядеть точно так же. Мы сделаем метод takeDrink, который также принимает value, но здесь происходит изменение.

Когда мы вызываем метод takeDrink, то вместо того, чтобы вернуть тот же стакан, который был у нас раньше, мы возвращаем новый стакан с новым content и amount. Чтобы сделать это, мы просто создаем new ImmutableGlass, передавая content и amount.

class ImmutableGlass {
  constructor(content, amount) {
    this.content = content;
    this.amount = amount;
  }

  takeDrink(value) {
    return new ImmutableGlass(this.content, Math.max(this.amount - value, 0));
  }
}

Теперь, если мы сравним значения после вызова takeDrink, то увидим, что они не равны. Так как ig1.amount не изменилось, а вот ig2.amount поменяло свое значение.

const ig1 = new ImmutableGlass("water", 100);
const ig2 = ig1.takeDrink(20);
console.log(ig1 === ig2); // false
console.log(ig1.amount === ig2.amount); // false
console.log(ig1.amount); // 100
console.log(ig2.amount); // 80

Website, name & logo
Copyright © 2022. Alex Myzgin