Записи и Кортежи
В прошлом посте я писал о пайплайн операторе, который будет использоваться для создания композиций функций.
В этой статье пойдёт речь о новых типах данных: записях и кортежах, которые появятся в будущем. Они позволят эффективнее писать код в функциональном стиле.
На момент написания статьи записи и кортежи находятся в стадии Stage 2 и не включены в стандарт языка ECMAScript.
Предыстория
Одна из самых сложных для дебага вещей при создании приложений — это отслеживание мутаций, проблемы с побочными эффектами и сохранение состояния приложения.
Рассмотрим пример:
const tokyo = { lat: 35.689, lon: 139.691 }
const tehran = tokyo
tehran.lon = 51.421
console.log(tokyo === tehran) // true
console.log(tokyo) // { lat: 35.689, lon: 51.421 }
В этом примере мы «случайно» мутировали наш изначальный объект tokyo
и получили баг.
Это произошло потому, что в JavaScript объекты работают так: при копировании объекта на самом деле создаётся ссылка на оригинальный объект, а не новая копия. Поэтому, изменяя копию, изменяется и оригинал.
Другой пример с массивами:
const travelPlan = ['Paris', 'Brussels', 'Amsterdam', 'Berlin']
const citiesToVisit = travelPlan.sort()
console.log(travelPlan) // [ 'Amsterdam', 'Berlin', 'Brussels', 'Paris' ]
В этом примере метод sort()
мутировал массив с нашим исходным маршрутом и сделал план путешествия по Европе неоптимальным.
Для того чтобы объект или массив стал неизменяемым (иммутабельным), разработчики прибегают к использованию сторонних библиотек, таких как Immutable.
Map
в Immutable аналогичен объекту, а List
— массиву, но оба они неизменяемы.
import { List, Map } from 'immutable'
const tokyo = Map({ lat: 35.689, lon: 139.691 })
const tehran = tokyo.set('lon', 51.421)
console.log(tokyo === tehran) // false
console.log(tokyo.toJS()) // { lat: 35.689, lon: 139.691 }
const travelPlan = List(['Paris', 'Brussels', 'Amsterdam', 'Berlin'])
const citiesToVisit = travelPlan.sort()
console.log(travelPlan.toJS()) // [ 'Paris', 'Brussels', 'Amsterdam', 'Berlin' ]
В примере с Immutable проблема отсутствует, поскольку объекты нельзя изменить.
Какие проблемы решает иммутабельность:
- Предотвращение побочных эффектов. Иногда при работе с изменяемыми данными возникают ситуации, когда изменение данных в одной части приложения приводит к непредсказуемым проблемам в другой части приложения.
- Поскольку иммутабельные данные делают поведение кода предсказуемым, то и тестирование таких приложений упрощается.
- Использование неизменяемых данных способно улучшить производительность. Например, если приложение написано на React, это может избавить приложение от лишних перерендеров.
Иммутабельность — одна из базовых концепций функционального программирования, где функции — чистые, отсутствуют побочные эффекты и функции возвращают предсказуемый результат.
В JavaScript 8 типов: Число, BigInt, Строка, Булевый тип, Null, Undefined, Символ и Объект.
В дополнение к ним планируется добавить два новых примитивных типа: Запись и Кортеж. Эти типы как раз призваны решать проблемы с иммутабельностью.
Предварительная Настройка
Записи и кортежи не поддерживаются ни одним из браузеров. Поэтому, чтобы начать их использовать, потребуется настроить Babel.
Для начала установим сам Babel:
npm install --save-dev @babel/cli @babel/core
И установим плагин для поддержки записей и кортежей и полифил:
npm install --save-dev @babel/plugin-proposal-record-and-tuple @bloomberg/record-tuple-polyfill
Также добавим настройки плагина в .babelrc
:
{
"plugins": [
[
"@babel/plugin-proposal-record-and-tuple",
{
"importPolyfill": true
}
]
]
}
И выполним команду babel ./index.js -d dist && node ./dist/index.js
.
Как Работают Записи и Кортежи
По синтаксису записи и кортежи схожи с объектами и массивами, но в начале объявления используется символ #
.
Например:
const tokyo = #{ lat: 35.689, lon: 139.691 }
const travelPlan = #['Paris', 'Brussels', 'Amsterdam', 'Berlin']
Иммутабельность
Записи и кортежи нельзя расширить или изменить. При попытке изменить запись или кортеж произойдёт ошибка.
let colors = #['red', 'green', 'blue']
colors[0] = 'yellow' // TypeError: Cannot assign to read only property '0' of object '[object Tuple]'
colors.push('purple') // TypeError: colors.push is not a function
Примитивность
Записи и кортежи — примитивы. Это значит, что когда сравниваются переменные с такими типами данных, имеет значение их содержимое, а не на то, где они хранятся в памяти компьютера.
{ name: 'John', age: 28 } === { name: 'John', age: 28 } // false
#{ name: 'John', age: 28 } === #{ name: 'John', age: 28 } // true
['John', 'Jane'] === ['John', 'Jane'] // false
#['John', 'Jane'] === #['John', 'Jane'] // true
При сравнении записей не важен порядок ключей:
#{ type: 'cat', color: 'black' } === #{ color: 'black', type: 'cat' } // true
Ограничения
Поскольку записи и кортежи — примитивы, в качестве значений или ключей они могут хранить только другие примитивы.
Здесь будет ошибка:
// TypeError: cannot use an object as a value in a record
const order = #{
id: 1,
price: 200,
items: ['apple', 'pear', 'orange'],
}
Однако записи и кортежи могут содержать в себе другие записи и кортежи:
// OK
const order = #{
id: 1,
price: 200,
items: #['apple', 'pear', 'orange'],
}
Заключение
Как видно из примеров, отсутствие иммутабельности зачастую приводит к нежелательным изменениям и путанице в поведении кода. Неожиданные изменения данных создают сложные для отслеживания ошибки и непредсказуемое поведение приложения.
Иммутабельность предоставляет чёткий и контролируемый способ управления данными, уменьшая риск подобных проблем.
Записи и кортежи позволят строить надёжные и предсказуемые приложения. Они уменьшают риск возникновения таких ошибок, улучшают архитектуру и предотвращают случайные перерендеры при использовании JS фреймворков.