Константные структуры


Все никак не могу остановиться говорить про иммутабельность. Дело в том, что буквально недавно закрыли вакансию Node.js TypeScript разработчика в один интересный криптовалютный стартап. И было примечательно то, что собеседующие техлиды очень щепетильно относились к тому, на сколько хорошо кандидат понимает принципы иммутабельности. Это было одним из важнейших критериев. Помимо всего этого от кандидата ожидали хорошей базовой подготовки в computer science, принципов и паттернов ООП, принципов SOLID и прочие вещи. Сегодняшняя статья закрывающая по теме констант и константных неизменяемых объектов.

Один из юзкейсов использования const для объектов — это создание иммутабельной структуры данных. И одна из таких структур — кортеж (tuples).

Сейчас во многих языках программирования существует такая конструкция. Где-то кортежи встроены в язык, а где-то реализуются средствами библиотек. Кортежи есть, например, в языках Erlang, F#, Groovy, Haskell, Lisp, C#, D, Python, Ruby, Go, Rust, Swift и многих других…

Что такое кортеж?

Кортеж (tuple) — упорядоченный набор фиксированной длины.

Явная реализация кортежа в JavaScript

Так как в JS нет синтаксической конструкции для объявления кортежа, мы создадим функцию tuple (прям как в Python):

const tuple = (...args) => Object.freeze(args);

Ну вот и все. Пример использования:

const tup = tuple ( 1, 2, 3, 4 );
tup[0] = 13; // ничего не произойдет
console.log( tup ); // [1,2,3,4]

Мы получили неизменяемый список фиксированной длины. Так просто? И ради этой строчки столько TL;DR написано в этой статье? Ну выходит что да. Но ведь мало знать решение, неплохо бы понимать откуда это взялось и зачем может понадобиться.

Зачем?

TL;DR теория и занудство

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

Так зачем еще и кортежи? А в языках со слабой типизацией, в таком как JavaScript, например — и тем более. Разница между кортежами и списками вообще не видна. Разница ощутима в других языках, где кортежи являются встроенными структурами (например в Python) либо в компилируемых языках. В них кортежи являются оптимизированными структурами данных, позволяя не только защищать данные, но и экономить ресурсы.

Если вы вошли в мир программирования с языков со слабой типизацией, все вышесказанное может ввести вас в некоторое заблуждение. Да и действительно, зачем нам кортежи в JavaScript?

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

В языках программирования со статической типизацией кортеж отличается от списка тем, что элементы кортежа могут принадлежать разным типам и набор таких типов заранее определён типом кортежа, а значит и размер кортежа также определён. С другой стороны, коллекции (списки, массивы) имеют ограничение по типу хранимых элементов, но не имеют ограничения на длину. Такое можно реализовать в TypeScript. Но сейчас речь о ванильном JS.

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

Неявные кортежи в JavaScript

На самом деле вы уже используете кортежи в JavaScript неявно и даже не подозреваете об этом. Кортежи неявно используются во всех языках программирования, даже в Си и Ассемблере.

Список аргументов функции или список инициализации массива является неявным кортежем

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

Список инициализации массива — это тоже кортеж. Даже обычный блок кода — это тоже кортеж. Только элементами его являются не значения или объекты, а синтаксические конструкции.

Пара — частный случай кортежа

В С++ есть частный случай реализации кортежа — структура данных, называемая пара. Как понятно из названия пара может содержать только 2 значения, в то время как кортеж — любое количество.

Реализация пары на JavaScript:

const pair = (...args) => Object.freeze( args.slice(0,2) );
// или так, ведь мы знаем что у нас могут быть только 2 аргумента
const pair = (x, y) => Object.freeze([x, y]);

Пример использования:

const par = pair ( 1, 2, 3 ); // 3 не добавится
par[0] = 3; // нельзя изменить
par[4] = 4; // нельзя добавить
console.log(par); // [1,2]

Кортежи в TypeScript

TypeScript позволяет затипизировать структуру данных и дает нам возможность описать кортеж, содержащий разные типы. Допустим мы хотим кортеж, который содержит сразу два типа:

const myTuple: [string, number] = ['foo', 123];
myTuple[0] = 123; // Type '123' is not assignable to type 'string'.

Так же кортеж можно описать через интефейс, расширив базовый тип Array:

interface MyTuple<T,U> extends Array<T|U> {
0: T;
1: U;
}
const mytuple: MyTuple<boolean, number> = [true, 123];

Таким образом можно создавать списки с фиксированными типами. Но если вспомнить что такое кортеж — это упорядоченный набор фиксированной длины. А у нас получились объекты, в которые можно добавить новые элементы.

Упорядоченный набор фиксированной длины

Мы можем указать длину нашего кортежа и TS будет проверять ее:

interface MyTuple extends Array<number | string> {
0: number;
1: string;
length: 2; // это литеральный тип '2', это не значение!
}
const mytuple: MyTuple = [123, 'abc', 'foo'];
// Type '[number, string, string]' is not assignable to type 'Tuple'. Types of property 'length' are incompatible. Type '3' is not assignable to type '2'.

Как видите, TS говорит нам, что мы не можем объявить кортеж с тремя элементами, так как мы заявили всего 2. Но, как помните из предыдущих упоминаний, все это артефакты. Поэтому для рантайма нам нужно добавить заморозку объекта.

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

interface MyTuple extends ReadonlyArray<string|number> {
0: string;
1: number;
length: 2;
}
const tup: MyTuple = <MyTuple>Object.freeze(['foo', 123]);
// или
const tup: MyTuple = Object.freeze(['foo', 123]) as MyTuple;
// или
const tup: MyTuple = ['foo', 123]; Object.freeze(tup);

Описание кортежа таким способом дает следующие преимущества:

  • нельзя создать изначально кортеж больше заданной длины
const tup: MyTuple = <MyTuple>Object.freeze(['foo', 123, 456]);
Type '[string, number, number]' is not assignable to type 'MyTuple'.
Types of property 'length' are incompatible.
Type '3' is not assignable to type '2'.
  • типы элементов упорядочены и их нельзя менять местами
tup[0] = 123;
Type '123' is not assignable to type 'string'
  • нельзя модифицировать существующие элементы
  • нельзя добавлять новые элементы
tup[3] = 456;
Index signature in type 'MyTuple' only permits reading

The END

На сегодня TypeScript стал очень популярным и его выбирают бэкенд разработчики на разных языках со строгой и явной типизацией. Поэтому есть отдельные вакансии TypeScript разработчиков.