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

Линейные и аффинные типы

Use-after-free, double-free, data races - три класса багов, уничтожившие репутацию C и C++. Mozilla подсчитала: 70% CVE в браузерах Chrome и Firefox - это memory safety баги. Rust решил их не через GC, не через runtime checks, а через систему типов. В основе - линейная логика 1987 года. Теория наконец встретила индустрию.

  • **Rust в Linux kernel (2022):** первый язык после C, принятый в ядро. Borrow checker - главная причина выбора.
  • **Android (Google):** новый код на Rust. Memory safety баги упали с 76% до 24% среди security уязвимостей за 3 года.
  • **Haskell LinearTypes (GHC 9.0+):** безопасная работа с файлами, сетью, CUDA без утечек ресурсов

Линейная логика Жирара

В классической логике если у нас есть доказательство P, его можно использовать сколько угодно раз: и дублировать, и выбрасывать. **Линейная логика**, предложенная Жан-Ивом Жираром в 1987 году, убирает это допущение: каждый ресурс должен быть использован *ровно один раз*.

Линейная логика моделирует **ресурсы**: деньги, файловые дескрипторы, токены доступа, память. В отличие от классической логики, здесь «использовать» значит «потребить». Это даёт фундамент для систем типов, отслеживающих жизненный цикл ресурсов.

**Аффинные типы** - ослабление линейных: ресурс можно использовать *не более одного* раза (то есть можно и выбросить, но нельзя дублировать). Rust использует именно аффинные типы через систему владения.

В линейной логике что означает оператор ⊸ (линейная импликация, A ⊸ B)?

Управление ресурсами через типы

Файловый дескриптор - классический пример ресурса с lifecycle: открыть, использовать, закрыть ровно один раз. Забыть закрыть - утечка. Закрыть дважды - undefined behavior. Линейные типы кодируют этот protocol в системе типов: компилятор отслеживает использование.

Ключевое: компилятор статически гарантирует, что каждый `Handle` будет закрыт *ровно раз* - без runtime-проверок, без GC finalizers, без надежды на дисциплину разработчика. Это **protocol compliance через типы**.

Что произойдёт в системе с линейными типами, если файловый handle не закрыт перед выходом из функции?

Borrow Checker Rust как линейные типы

Rust не называет свою систему «линейными типами», но её семантика - прямое воплощение аффинных типов. Каждое значение имеет ровно одного **владельца** (owner). Когда владелец выходит из scope, значение уничтожается. Передача значения = **move**: оригинал больше недоступен.

Правило «либо N shared, либо 1 exclusive» - это ограничение **aliasing**: не может быть двух mutable ссылок на одно значение. Именно aliasing является причиной большинства data races и use-after-free багов. Borrow checker исключает их статически.

**Lifetimes в Rust** - ещё один слой: компилятор отслеживает, что ссылка не переживает объект, на который ссылается. `'a` в `&'a str` - это аннотация времени жизни, позволяющая компилятору доказать отсутствие dangling references.

Почему Rust разрешает множество `&T` одновременно, но только один `&mut T`?

Uniqueness Types в Clean и Idris

**Uniqueness types** - двойник линейных типов, разработанный для функциональных языков. Идея: тип помечается как *уникальный*, если гарантировано, что на него нет других ссылок в данный момент. Уникальный объект можно безопасно мутировать - как в imperative языках, но со статической гарантией.

Uniqueness types и линейные типы решают одну задачу с разных сторон. Линейные: «значение должно быть использовано ровно раз» (взгляд со стороны потребителя). Uniqueness: «на это значение нет других ссылок» (взгляд со стороны владельца). В Clean uniqueness позволяет делать destructive update массивов с O(1) вместо O(n) - без нарушения чистоты.

**Linearity в Haskell GHC 9.0+:** расширение `LinearTypes` добавляет `%1 ->` синтаксис. Это не полноценные линейные типы, но позволяет писать протоколы safe resource handling в обычном Haskell коде.

Линейные типы и uniqueness types - это одно и то же, просто разные названия

Они двойственны (dual), но не идентичны. Линейные типы ограничивают потребителя: «используй ровно раз». Uniqueness типы ограничивают поставщика: «нет других ссылок». В теории Girard эти системы связаны через логическую двойственность, но на практике дают разные паттерны программирования.

В Clean uniqueness дают O(1) мутацию в функциональном языке. В линейных типах акцент - на lifecycle ресурсов. Rust сочетает оба: ownership (uniqueness) + borrows (временное ослабление уникальности).

Почему uniqueness types позволяют делать destructive update массива в чисто функциональном языке (Clean)?

Линейные и аффинные типы

  • Линейная логика Жирара (1987): каждый ресурс используется ровно раз - основа для типовых систем управления ресурсами
  • Линейные типы в коде: handle должен быть и открыт, и закрыт - компилятор проверяет, не тесты
  • Rust borrow checker: аффинные типы + алиасинг-правило = нет data races и use-after-free статически
  • Uniqueness types (Clean, Idris): O(1) мутация в функциональном языке без нарушения чистоты

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

Линейные типы пересекаются с управлением памятью, параллелизмом и теорией типов.

  • Зависимые типы — Другой подход к статическим гарантиям - типы кодируют свойства значений
  • Ownership и управление памятью — Rust ownership - практическое воплощение аффинных типов
  • IO монады — Монада IO в Haskell использует похожую идею: World-token для упорядочивания эффектов

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

  • Линейные типы гарантируют «ровно раз», аффинные - «не более раз». В каких случаях важна именно нижняя граница (обязательное использование)?
  • Borrow checker позволяет несколько &T одновременно, но только один &mut T. Как это правило связано с концепцией aliasing в линейной логике?
  • Uniqueness types позволяют мутацию в функциональном языке. Как это влияет на тестируемость и рассуждения об инвариантах кода?

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

  • plt-13-ownership — Rust ownership is linear types made practical: each value used exactly once unless explicitly cloned
  • plt-09-dependent-types — Linear and dependent types represent orthogonal axes of type expressiveness; pairing them gives full picture
  • plt-06-lambda-calculus — Linear logic is a substructural extension of lambda calculus; the lambda abstraction rules are directly modified
  • plt-02-type-systems — Substructural type systems modify the structural rules (weakening, contraction) - need baseline type system knowledge
  • os-07-memory
Линейные и аффинные типы

0

1

Войти