Теория языков программирования

Дженерики и параметрический полиморфизм

До Java 5 каждый контейнер работал через `Object`: `list.get(0)` возвращал `Object`, и разработчик вручную кастил к нужному типу. Пропустить cast - ClassCastException в рантайме. Дженерики перенесли эту проверку в компилятор. В Rust monomorphization делает дженерики буквально бесплатными - zero overhead по сравнению с ручным кодом.

  • **Java Collections** - `List<String>`, `Map<K,V>` избавили от бесчисленных `(String) list.get(0)` и ClassCastException
  • **Rust Vec<T>, HashMap<K,V>** - monomorphization даёт производительность, неотличимую от написанного вручную специализированного кода
  • **TypeScript generics** - `Promise<T>`, `Array<T>`, React `useState<T>` - весь современный TS API строится на дженериках

Параметрический полиморфизм: одна реализация для всех типов

Функция `identity` принимает значение и возвращает его обратно. На Java 1.4 это писали отдельно для `int`, `String`, `User`. С дженериками - один раз: `id: ∀T. T → T`. Компилятор проверяет корректность для **любого** T без знания конкретного типа.

**Parametricity:** чем более параметрический тип, тем меньше возможных реализаций. Функция `∀T. [T] → [T]` может только переставлять/отфильтровывать элементы - она не может создать новый T из ниоткуда или заглянуть внутрь. Тип - это спецификация.

Функция имеет тип `<T>(items: T[], index: number) => T`. Что гарантирует её сигнатура?

Ограниченные type parameters: только типы с нужными операциями

Чисто параметрический `T` не позволяет ничего сделать со значением - нельзя сложить, сравнить, распечатать. **Ограничения (bounds)** говорят: «T - любой тип, но только если у него есть нужные операции». Разные языки выражают это по-разному.

ЯзыкМеханизм boundsПример
Javaextends / implements<T extends Comparable<T>>
TypeScriptextends (структурный)<T extends { length: number }>
RustTrait bounds<T: Display + Clone>
HaskellTypeclass constraints(Ord a, Show a) => a -> a
C++Concepts (C++20)template<std::sortable T>

Зачем нужны bounded type parameters в сравнении с чисто параметрическими `T`?

Вариантность: когда List<Cat> является List<Animal>?

Интуиция подсказывает: `List<Cat>` должен быть `List<Animal>` (ведь кот - животное). Но это ловушка. Если разрешить присвоить `List<Cat>` переменной типа `List<Animal>`, то через эту переменную можно добавить `Dog` в список котов - нарушение типобезопасности. Это проблема **вариантности**.

**Мнемоника PECS (Producer Extends, Consumer Super):** если используешь F как producer (только читаешь) - используй `extends` (ковариантность). Если как consumer (только пишешь) - `super` (контравариантность). Если и то, и другое - инвариантность.

Функция `printAll(List<? extends Animal> list)` принимает `List<Cat>`. Что НЕЛЬЗЯ делать внутри функции?

Type Erasure vs Reification vs Monomorphization

Дженерики можно реализовать тремя способами. Каждый - с разными trade-offs по скорости, размеру бинарника и возможностям рантайма.

ПодходЯзыкСутьПоследствие
Type ErasureJava, Kotlin (JVM)Типовые параметры удаляются при компиляции; в байткоде только Objectнельзя `instanceof List<String>`, нельзя `new T()`, один `.class`-файл
ReificationC# (.NET)Типы сохраняются в рантайме; CLR знает `List<int>` vs `List<string>`можно `typeof(T)`, reflection работает с type args, отдельный JIT per specialization
MonomorphizationRust, C++ templatesКомпилятор генерирует отдельный код для каждого конкретного Tбыстрейший рантайм (нет boxing), большой бинарник (code bloat)

Дженерики Java и Rust работают одинаково - просто синтаксический сахар над Object.

Java использует type erasure (один класс для всех T), Rust - monomorphization (отдельный машинный код per T). Это принципиально разные реализации с разными возможностями и trade-offs по производительности.

Java generics добавили в версии 5 с сохранением совместимости со старым байткодом - поэтому erasure. Rust был спроектирован с нуля без этого ограничения, monomorphization даёт zero-cost abstractions.

В Java нельзя написать `new T()` в generic-методе. Почему?

Дженерики и параметрический полиморфизм

  • **Parametric polymorphism:** одна реализация для любого T; тип-сигнатура ограничивает возможные реализации (parametricity)
  • **Bounded parameters:** `T: Comparable` / `T extends Sortable` - T может быть любым, но только с нужными операциями
  • **Вариантность:** ковариантность (producer: только читаем), контравариантность (consumer: только пишем), инвариантность (и то, и другое)
  • **Реализации:** Java erasure (один класс, нет рантайм-типа), C# reification (тип сохранён, reflection), Rust monomorphization (код per type, zero overhead)

Связанные темы

Дженерики - фундамент для абстракций высшего порядка и zero-cost abstractions.

  • Алгебраические типы данных — Maybe<T> и Result<T,E> - sum types с type parameters
  • Типы высшего порядка (Higher-Kinded Types) — Functor<F<_>> - дженерики над дженериками

Вопросы для размышления

  • Почему List<Cat> не является подтипом List<Animal> при инвариантности, но функция `(Animal) => void` является подтипом `(Cat) => void` (контравариантность)?
  • В каких сценариях type erasure Java - преимущество, а не ограничение?
  • Как monomorphization в Rust влияет на время компиляции и размер бинарного файла при использовании большого числа generic типов?

Связанные уроки

  • plt-07-algebraic-types — Generic ADTs are the most common use of generics; need ADT foundation first
  • plt-09-dependent-types — Dependent types generalize generics: types can depend on values, not just other types
  • plt-11-typeclasses — Higher-kinded generics power typeclass abstractions like Functor and Monad
  • plt-12-subtyping — Variance (covariance, contravariance) is where generics and subtyping interact
  • ml-01-intro
Дженерики и параметрический полиморфизм

0

1

Войти