Записи и Кортежи

5 минут на прочтение
Также переведено на:

В прошлом посте я писал о пайплайн операторе, который будет использоваться для создания композиций функций.

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

На момент написания статьи записи и кортежи находятся в стадии Stage 2 и не включены в стандарт языка ECMAScript.

Предыстория

Одна из самых сложных для дебага вещей при создании приложений — это отслеживание мутаций, проблемы с побочными эффектами и сохранение состояния приложения.

Рассмотрим пример:

JavaScript
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 объекты работают так: при копировании объекта на самом деле создаётся ссылка на оригинальный объект, а не новая копия. Поэтому, изменяя копию, изменяется и оригинал.

Другой пример с массивами:

JavaScript
const travelPlan = ['Paris', 'Brussels', 'Amsterdam', 'Berlin']
const citiesToVisit = travelPlan.sort()

console.log(travelPlan) // [ 'Amsterdam', 'Berlin', 'Brussels', 'Paris' ]

В этом примере метод sort() мутировал массив с нашим исходным маршрутом и сделал план путешествия по Европе неоптимальным.

Для того чтобы объект или массив стал неизменяемым (иммутабельным), разработчики прибегают к использованию сторонних библиотек, таких как Immutable.

Map в Immutable аналогичен объекту, а List — массиву, но оба они неизменяемы.

JavaScript
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 проблема отсутствует, поскольку объекты нельзя изменить.

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

  1. Предотвращение побочных эффектов. Иногда при работе с изменяемыми данными возникают ситуации, когда изменение данных в одной части приложения приводит к непредсказуемым проблемам в другой части приложения.
  2. Поскольку иммутабельные данные делают поведение кода предсказуемым, то и тестирование таких приложений упрощается.
  3. Использование неизменяемых данных способно улучшить производительность. Например, если приложение написано на React, это может избавить приложение от лишних перерендеров.

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

В JavaScript 8 типов: Число, BigInt, Строка, Булевый тип, Null, Undefined, Символ и Объект.

В дополнение к ним планируется добавить два новых примитивных типа: Запись и Кортеж. Эти типы как раз призваны решать проблемы с иммутабельностью.

Предварительная Настройка

Записи и кортежи не поддерживаются ни одним из браузеров. Поэтому, чтобы начать их использовать, потребуется настроить Babel.

Для начала установим сам Babel:

Bash
npm install --save-dev @babel/cli @babel/core

И установим плагин для поддержки записей и кортежей и полифил:

Bash
npm install --save-dev @babel/plugin-proposal-record-and-tuple @bloomberg/record-tuple-polyfill

Также добавим настройки плагина в .babelrc:

JSON
{
  "plugins": [
    [
      "@babel/plugin-proposal-record-and-tuple",
      {
        "importPolyfill": true
      }
    ]
  ]
}

И выполним команду babel ./index.js -d dist && node ./dist/index.js.

Как Работают Записи и Кортежи

По синтаксису записи и кортежи схожи с объектами и массивами, но в начале объявления используется символ #.

Например:

JavaScript
const tokyo = #{ lat: 35.689, lon: 139.691 }

const travelPlan = #['Paris', 'Brussels', 'Amsterdam', 'Berlin']

Иммутабельность

Записи и кортежи нельзя расширить или изменить. При попытке изменить запись или кортеж произойдёт ошибка.

JavaScript
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

Примитивность

Записи и кортежи — примитивы. Это значит, что когда сравниваются переменные с такими типами данных, имеет значение их содержимое, а не на то, где они хранятся в памяти компьютера.

JavaScript
{ 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

При сравнении записей не важен порядок ключей:

JavaScript
#{ type: 'cat', color: 'black' } === #{ color: 'black', type: 'cat' } // true

Ограничения

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

Здесь будет ошибка:

JavaScript
// TypeError: cannot use an object as a value in a record

const order = #{
  id: 1,
  price: 200,
  items: ['apple', 'pear', 'orange'],
}

Однако записи и кортежи могут содержать в себе другие записи и кортежи:

JavaScript
// OK

const order = #{
  id: 1,
  price: 200,
  items: #['apple', 'pear', 'orange'],
}

Заключение

Как видно из примеров, отсутствие иммутабельности зачастую приводит к нежелательным изменениям и путанице в поведении кода. Неожиданные изменения данных создают сложные для отслеживания ошибки и непредсказуемое поведение приложения.

Иммутабельность предоставляет чёткий и контролируемый способ управления данными, уменьшая риск подобных проблем.

Записи и кортежи позволят строить надёжные и предсказуемые приложения. Они уменьшают риск возникновения таких ошибок, улучшают архитектуру и предотвращают случайные перерендеры при использовании JS фреймворков.