Незмінюваність в 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', ", 'isRevealed'], true);
}

Тепер функція revealTile повертає новий незмінний примірник, в якому одна з плиток відрізняється від попередньої версії. setIn null-стійка і заповниться порожніми об'єктами, якщо яка-небудь з частин ключа не існує. У випадку з дошкою Сапера це не бажано, оскільки відсутня плитка означає, що ми намагаємося відкрити плитку поза дошки. Це можна пом'якшити, використовуючи getIn для пошуку плитки перед виконанням дій над нею:
function revealTile(game, tile) {
return game.getIn(['tiles', tile]) ?
game.setIn(['tiles', ", '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) {
// дані не змінилися, припинити
}

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

Висновки
Я сподіваюся, що в цій статті ви почерпнули певні знання про те, як незмінюваність допоможе вам поліпшити свій код, і що продовжений приклад може пролити світло на практичні аспекти роботи в даному напрямку. Незмінюваність набирає популярність і це буде не остання стаття з цієї теми, яку ви прочитаєте в цьому році. Спробуйте, і я обіцяю, що вона вам дуже швидко сподобається настільки ж, наскільки сподобалася мені.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.