Как стать автором
Поиск
Написать публикацию
Обновить

Кратко о вариантности с примерами на TypeScript

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров935

В теории типов вариантность описывает отношение между двумя обобщёнными типами (дженериками). Например, в каких обстоятельствах родительский тип может быть заменён дочерним, а в каких — нет, и так далее.

На эту тему можно найти множество ресурсов, особенно таких, где всё описано длинно и сложным, формально-архитектурным языком. Мне бы хотелось создать короткую и простую памятку (с небольшими вкраплениями формализмов), к которой можно легко вернуться, если вдруг забудутся детали.

Ковариантность

Отношение ковариантности представляет собой обычное отношение подтипа, когда более Узкий/Дочерний тип может использоваться там, где ожидается более Широкий/Родительский тип. Например:

Я могу поставить Кошку туда, где может стоять любое Животное
Но я не могу поставить любое Животное туда, где может стоять только Кошка

class Animal {
    genus: string;
}
class Cat extends Animal {
    clawSize: number;
}

function move(animal: Animal) {}
function meow(cat: Cat) {}

move(cat) // Любая кошка может двигаться
meow(animal) // Не каждое животное умеет мяукать

Точнее: Вы можете использовать B там, где ожидается A, если B < A.

// V — это позиция возвращаемого значения (выход)
type Covariant<V> = () => V;

// Где Animal — широкий тип (W), а Cat — узкий (N)
function covariance(
    covW: Covariant<Animal>,
    covN: Covariant<Cat>,
) {
  covW = covN; // OK. Функция, возвращающая кошку, может заменить функцию, возвращающую животное.
  covN = covW; // Ошибка! Нельзя быть уверенным, что функция, возвращающая животное, вернёт именно кошку.
}

Контравариантность

Контравариантность — это противоположность ковариантности. Это, пожалуй, самый сложный для понимания тип вариантности. В случае контравариантности, когда ожидается Узкий/Дочерний тип, вместо него можно использовать Широкий/Родительский.

В каких обстоятельствах это может произойти? Представьте себе обработчик. Например, обработчик общего корма для животных, который обогащает его белком (допустим что это полезно для любого животного). И обработчик для кошачьего корма, который придаёт ему более рыбный вкус (глупо, но неважно).

Итак, можно ли обработать кошачий корм с помощью общего обработчика корма для животных? Конечно, больше белка кошке не навредит.
А можно ли обработать любой корм для животных с помощью обработчика кошачьего корма? Думаю, нет — не все любят рыбный вкус.

Повторим более формально:

Я могу обработать Кошачий корм так же, как обрабатывается любой корм для Животных.
Но я не могу обработать корм для Животных так же, как обрабатывается Кошачий корм.

class AnimalFood {
  protein: number = 0
}
class CatFood extends AnimalFood {
  fishness: number = 0
}

function processAnimalFood(animalFood: AnimalFood): void {
  // Добавляем немного белка //
}
function processCatFood(catFood: CatFood): void {
  // Придаём рыбный вкус //
}

/**
 * Перед подачей обработаем корм
 */
function serveAnimalFood(processor: (food: AnimalFood) => void): void {
    const food = new AnimalFood();
    processor(food);
}
function serveCatFood(processor: (food: CatFood) => void): void {
    const food = new CatFood();
    processor(food);
}

// Мы не можем использовать обработчик кошачьего корма, чтобы подать корм для животного!
// Не все животные любят рыбный вкус!
serveAnimalFood(processCatFood);

// Вы можете использовать обработчик корма для животных, чтобы подать кошачий корм.
// Белок пойдет кошке на пользу
serveCatFood(processAnimalFood);

В теории типов: Вы можете использовать обработчик для A там, где ожидается обработчик для B, если B < A.

type Contravariant<V> = (v: V) => void;

// Где Animal — широкий тип (W), а Cat — узкий (N)
function contravariance(
    contraW: Contravariant<Animal>,
    contraN: Contravariant<Cat>,
) {
  contraW = contraN; // Ошибка! Обработчик кошачей еды не может обработать любую еду.
  contraN = contraW; // OK! Обработчик общей еды справится и с кошачей.
}

Инвариантность

С инвариантностью всё проще. Это представляет собой отсутствие взаимозаменяемости. В номинативных системах типов, например в С, это единственный вид вариантности. Реальный пример такого отношения можно найти в сортировке мусора.

Есть общее понятие Мусор и его разновидности, такие как Макулатура, Пищевые Отходы и т.д.
И если ваши отходы классифицированы, и для них есть подходящий контейнер, вы должны использовать этот и только этот контейнер.

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

class Waste {
  readonly type = 'неперерабатываемый';
}
class FoodWaste {
  readonly type = 'органика';
}

function unrecycledBin(waste: Waste) {}
function organicBin(waste: FoodWaste) {}

unrecycledBin(new FoodWaste()); // Нельзя выбрасывать пищевые отходы в контейнер для неперерабатываемых! Надо быть молодцом!
organicBin(new Waste()); // Нельзя выбрасывать несортированный мусор в контейнер для органики, вы что, преступник???

Формально: Вы можете использовать A только там, где ожидается A.

type Invariant<V> = (v: V) => V;

function invariance(
    inW: Invariant<Animal>,
    inN: Invariant<Cat>,
) {
  inW = inN; // Ошибка! Типы не взаимозаменяемы.
  inN = inW; // Ошибка! То же самое.
}

Бивариантность

Противоположность инвариантности. Бивариантность — это полная взаимозаменяемость, когда тип A можно заменить на B и наоборот.

В TypeScript бивариантность не распространена, но всё же встречается. Например, как выяснили ранее, параметры функций являются контравариантны. Но есть исключения: у методов параметры бивариантны.

type Bivariant<V> = {
    process(v: V): void;
}

function bivariance(
    biW: Bivariant<Animal>,
    biN: Bivariant<Cat>,
) {
  biW = biN; // OK!
  biN = biW; // OK!
}

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

// Ключевое слово `in` в дженериках делает тип Контравариантным
type ContravariantMethod<in V> = {
    process(v: V): void;
}

function contravariance(
    contraW: ContravariantMethod<Animal>,
    contraN: ContravariantMethod<Cat>,
) {
  contraW = contraN; // Ошибка! Теперь это строгая контравариантность.
  contraN = contraW; // OK!
}

Ссылки

Теги:
Хабы:
0
Комментарии8

Публикации

Ближайшие события