Неизменяемость в JavaScript

в 10:27, , рубрики: immutability, javascript, Программирование

Неизменяемость — основной принцип функционального программирования, который также может многое предложить объектно-ориентированным программам. В этой статье я расскажу вам о том, что именно является краеугольным камнем неизменяемости, как использовать эту концепцию в JavaScript и почему это полезно.

Что такое неизменяемость?

Книжное определение изменяемости звучит следующим образом: «склонность предмета к изменениям или преобразованиям». В программировании мы используем это слово, когда подразумеваем объекты, состояние которых можно изменить с течением времени. Неизменяемое значение — это с точностью до наоборот, оно уже никогда не изменится после создания.

Если это кажется странным, то позвольте вам напомнить, что большинство из тех значений, которые мы всё время используем, в действительности неизменяемы:

var statement = "I am an immutable value";
var otherStr = statement.slice(8, 17);    

Я думаю, что никто не удивится, если узнает, что вторая строчка никоим образом не изменят строковое значение statement. В действительности не существует строковых методов, которые бы изменяли строку над которой они выполняют действие, все они возвращают новые строки. Причина состоит в том, что строки неизменяемы — их нельзя преобразовывать, мы можем лишь создавать новые строки.

Строки — не единственные неизменяемые значения, встроенные в JavaScript. Числа также неизменяемы. Вы вообще можете себе представить окружение, где вычисление выражения 2 + 3 меняет значение числа 2? Это звучит абсурдно, хотя мы и делаем это всё время с нашими объектами и массивами.

В JavaScript изменяемость имеется в изобилии

В JavaScript строки и числа спроектированы быть неизменяемыми. Однако, рассмотрите следующий пример с использованием массивов:

var arr = [];
var v2 = arr.push(2); 

Каково значение v2? Если бы массивы вели себя сообразно строкам и числам, то v2 содержал бы новый массив с одним элементом внутри — 2. Однако это другой случай. Вместо этого была изменена ссылка arr дабы содержать число, а v2 содержит новую длину arr.

Вообразите себе тип ImmutableArray. Его поведение, заимствуя у чисел и строк, выглядело бы следующим образом:

var arr = new ImmutableArray([1, 2, 3, 4]);
var v2 = arr.push(5);
 
arr.toArray(); // [1, 2, 3, 4]
v2.toArray();  // [1, 2, 3, 4, 5]

Аналогичным образом неизменяемый ассоциативный массив, который можно было бы использовать вместо большинства объектов, обладал бы методами для «установки» свойств, которые ничего бы не устанавливали на самом деле, а возвращали бы новый объект с требуемыми изменениями:

var person = new ImmutableMap({name: "Chris", age: 32});
var olderPerson = person.set("age", 33);
 
person.toObject(); // {name: "Chris", age: 32}
olderPerson.toObject(); // {name: "Chris", age: 33}

Подобно тому как 2 + 3 не меняет значений ни 2 ни 3, празднование кем-либо своего 33го дня рождения, не отменяет той истины, что человек ранее был 32 лет отроду.

Неизменяемость в JavaScript на практике

У JavaScript (пока что) не имеется неизменяемых списков и ассоциативных массивов, поэтому сейчас нам потребуется сторонняя библиотека. Существуют 2 очень хорошие доступные библиотеки. Первая из них — Mori, которая позволяет применять постоянные структуры данных из ClojureScript, а также API поддержки в JavaScript. Второй — immutable.js, написанный разработчиками из Facebook. В этой демонстрации я буду применять immutable.js по той простой причине, что его API более знакомо JavaScript разработчикам.

В данной демонстрации мы рассмотрим принцип работы с неизменяемыми данными в Сапёре. Доска представлена неизменяемым ассоциативным массивом, в котором tiles являются наиболее интересной частью данных. Это — неизменяемый список из неизменяемых ассоциативных массивов, где каждый из последних (т. е. ассоц. масс. — прим. пер.) представляет отдельную плитку на доске. Вся конструкция инициализируется с помощью объектов и массивов JavaScript, а затем становится «бессмертной» благодаря функции fromJS из immutable.js:

function createGame(options) {
  return Immutable.fromJS({
    cols: options.cols,
    rows: options.rows,
    tiles: initTiles(options.rows, options.cols, options.mines)
  });
}

Остальная часть ядра игровой логики реализована в виде функций, которые берут эту неизменяемую структуру в качестве своего первого аргумента и возвращают новый экземпляр. Наиболее важной функцией является revealTile. При вызове она помечает плитку как открытую, чтобы открыть её. С изменяемой структурой данных, это будет очень просто:

function revealTile(game, tile) {
  game.tiles[tile].isRevealed = true;
}

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

function revealTile(game, tile) {
  var updatedTile = game.get('tiles').get(tile).set('isRevealed', true);
  var updatedTiles = game.get('tiles').set(tile, updatedTile);
  return game.set('tiles', updatedTiles);
}

Фе! К счастью, подобные вещи — нередкое явление. Поэтому в нашем инструментарии есть метод для подобных целей:

function revealTile(game, tile) {
  return game.setIn(['tiles', tile, 'isRevealed'], true);
}

Теперь функция revealTile возвращает новый неизменяемый экземпляр, в котором одна из плиток отличается от предыдущей версии. setIn null-устойчива и заполнится пустыми объектами, если какая-либо из частей ключа не существует. В случае с доской Сапёра это не желательно, поскольку отсутствующая плитка означает, что мы пытаемся открыть плитку вне доски. Это можно смягчить, используя getIn для поиска плитки перед выполнением действий над нею:

function revealTile(game, tile) {
  return game.getIn(['tiles', tile]) ?
    game.setIn(['tiles', tile, 'isRevealed'], true) :
    game;
}

Если плитка не существует, то мы просто возвращаем существующую игру. Это было краткое знакомство с неизменяемостью на практике, если хотите разобраться тщательнее, перейдите на этот codepen, там содержится полная реализация правил игры Сапёр.

А что с производительностью?

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

Поскольку неизменяемые объекты никогда не меняются, они могут быть реализованы с помощью стратегии, называемой «общие структуры» (structural sharing), которая порождает гораздо меньшую издержку в затратах на память, чем вы могли бы ожидать. В сравнении со встроенными массивами и объектами издержка все ещё будет существовать, но она будет иметь фиксированную величину и обычно может компенсироваться другим преимуществами, доступными благодаря неизменяемости. На практике, во множестве случаев применение неизменяемых данных увеличит общую производительность вашего приложения, даже если определённые операции станут более затратными в отдельности.

Улучшенное отслеживание изменений

В любом UI фреймворке одной из самых сложных задач является поиск мутаций. Это настолько широкоизвестное испытание, что EcmaScript 7 предоставляет отдельный API дабы помочь отслеживать мутации объекта с лучшей производительностью: Object.observe(). В то время как одним людям этот API по душе, другим кажется, что это ответ не на тот вопрос. В любом случае он не решает проблему отслеживания мутаций должным образом:

var tiles = [{id: 0, isRevealed: false}, {id: 1, isRevealed: true}];
Object.observe(tiles, function () { /* ... */ });
 
tiles[0].id = 2;

Мутация объекта tiles[0] не приводит в действие наш обозреватель мутаций, следовательно, предложенный механизм отслеживания мутаций не годится даже для тривиального случая применения. Каким образом неизменяемость может помочь в данной ситуации? Предположим, что у приложения состояние а, а у потенциально нового приложения состояние b:

if (a === b) {
  // данные не изменились, прекратить
}

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

Выводы

Я надеюсь что в этой статье вы почерпнули определённые знания о том, как неизменяемость поможет вам улучшить свой код, и что продолженный пример может пролить свет на практические аспекты работы в данном направлении. Неизменяемость набирает популярность и это будет не последняя статья по данной теме, которую вы прочтёте в этом году. Попробуйте, и я обещаю, что она вам очень быстро понравится настолько же, насколько понравилась и мне.

Автор: Elusive_Dream

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js