Immutable данные и немного JavaScript

HanSolo

Обычно чаще, чем реже, мы объявляем переменные из расчёта, что их значение будет меняться: счётчики будут увеличиваться, массивы дополняться, объект типа Person поменяет богопротивное имя Джон на Аркадий, и т.д. Всё меняется, а  const  и   readonly нужны только жмотам и студентам, начитавшимся Макконела.

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

Идея, на первый взгляд, дебильная. Но она даёт интересные преимущества.

Например, хотя мы редко задумываемся об этом, у концепции изменяемых полей есть своя цена. С ними постоянно приходится думать:

  • а какое там сейчас значение?
  • где это значение может поменяться и на что?
  • нужное значение уже появилось, или будет после определенного момента?
  • а что если значение будет меняться в несколько этапов, а в середине что-то пошло не так?
  • если кто угодно может изменить поля, то как же это кэшировать?

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

Посмотрим на такой очень схематичный пример. Откуда-то пришли GPS координаты gpsCoordinates, и мы хотим перевести их в оконные screenCoordinates, потому что будем рисовать:

В очень абстрактном случае вот, что может пойти не так во время цикла:

  • Переменной gpsCoordinates  присвоят другой массив. Это может произойти, например, из  GPSToScreen — любовь к побочным эффектам иногда творит чудовищные артефакты. И внезапно мы перебираем новую коллекцию но в старый массив.
  • Оттуда же — из  gpsCoordinates удалили элемент. Теперь  i  индекс автоматически сместился вперед, и никто не заметил.
  • Десятью строками выше кто-то, оказывается, уже объявил переменную currentCoordinate, которую мы только что два раза перезаписали. Удачи в дебаггинге!
  • Если я по старой привычке напишу <= вместо < при проверке  gpsCoordinates.length и тем самым проскочу границу массива, а GPSToScreen не свалится с  undefined на входе, то в коллекцию результатов пойдёт вообще непонятно что и свалится с ошибкой где-нибудь в процессе рисования.
  • Если GPSToScreen бросит ошибку, и после этого никто не догадается почистить полузаполненный screenCoordinates, то во время рендеринга случится приятный сюрприз.
  • И т.д. В любом случае — удачи в дебаггинге!

Все эти ошибки хотя бы раз случались со мной. С read-only данными они исчезают как вид.

Вот каким можно было бы сделать предыдущий пример:

Для верности gpsCoordinates можно защитить рекурсивным Object.freeze, но это на случай, если мы совсем никому не верим.

По итогу получилось меньше кода, он стал безопаснее и понятнее. Есть всего две переменные (да, я помню, что они теперь константы), и зависимость одной от другой визуально легко отслеживается. Тотальный вин.

С редактированием сложных объектов такой же принцип: если нужно изменить какое-то свойство — делаем копию объекта и меняем свойство уже в ней.

Например, есть объект Cursor, который может двигаться вверх-вниз по списку:

Как он работает:

А вот как бы выглядела его immutable версия:

up и down теперь возвращают новый Cursor, и в жизни это выглядит так:

Теперь cursor  абсолютно предсказуемый. В коде есть только одно место, где он получает своё значение, и к тому же он случайно получил fluent интерфейс. И всё ценой небольшой ломки мозга.

Почему immutable данные полезны:

  • С ними код проще.
  • С ними меньше багов. Сложнее код — больше багов. Проще код — меньше багов.
  • Объекты создаются атомарно. То есть либо он создался успешно, либо не создался вовсе. [].map либо вернёт новый массив, либо упадёт с ошибкой.
  • Уменьшается эффект временного связывания — это когда порядок инициализации и доступа к полям важен.
  • Просто кэшировать — ничего же не меняется.
  • Проще тестировать. У read-only объекта не там много вариантов, что с ним может произойти
  • Потоки! Если приходится иметь дело с потоками, то для синхронизации доступа к read-only данным нужно просто ничего не делать.

Какие могут быть проблемы с immutable:

  • Нагрузка на мозг. Это всё-таки непривычный подход, и нужно ломать устоявшиеся привычки. Но оно проходит. Привыкли же мы жить без go-to. Привыкли же?
  • Нагрузка на CPU. Больше выделений памяти, больше сборки мусора, больше инициализаций объектов. Но! Есть мнение, и оно не только моё, что мы экономим на коде, который раньше приходилось писать для защиты изменяемых данных
  • Нагрузка на память. Будет больше временных объектов, это да. Будет больше сборки мусора. Это тоже. Но! Так как все временные объекты  — коротко живущие, то они пойдут в нулевое поколение сборщика мусора, которое чистится очень быстро. В отличие от фрагментированное первого поколения, в котором со временем оседают наши изменяемые объекты.

Конечно, immutable данные это не панацея, и голод в Африке они не искоренят. Да и совать такой подход во все сферы жизни тоже не стоит. Но избегать изменяемых переменных там, где без них можно спокойно обойтись, и делать read-only общие данные в потоках точно не повредит.

Immutable данные и немного JavaScript: 2 комментария

  1. В мире Java эта идея тоже гуляет. На сравнительно недавной конференции JEEConf выступал товарищ с таким же докладом: http://jeeconf.com/program/how-immutability-helps-in-oop/ (на ТыТруба есть видео), очень книжку рекомендовал от мелкософта: «Object Thinking» и свою написал на ту же тему. Слушал я и думал: новый тренд или давно забытое старое? не так часто я такой подход встречал именно в ООП, больше свойственно функциональным ЯП, имхо.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *