Skip to content

RFC-011a: Реализация интерфейсов и динамическая диспетчеризация ​

Родительский RFC: RFC-011: Проектирование системы обобщений (generics)

Настоящий RFC дополняет и заменяет разделы §2.1-2.4 RFC-011, посвящённые ограничениям интерфейса.

Краткое содержание ​

RFC-011 определяет систему обобщений, но не детализирует механизм реализации интерфейсов. Настоящий документ дополняет:

  1. Объявление интерфейса: интерфейс — это параметризованный тип — (Self: Type) -> Type, при реализации в него передаётся конкретный тип
  2. Реализация методов: поддерживается как внутреннее, так и внешнее объявление
  3. Правила перегрузки: разные сигнатуры разрешают перегрузку, одинаковые сигнатуры приводят к ошибке (переопределение запрещено)
  4. Значения по умолчанию: после поля сразу пишется = value
  5. Динамическая диспетчеризация: сбор типов на этапе compile-time + сопоставление интерфейса, без виртуальной таблицы

Основной замысел:

yaoxiang
# Определение интерфейса (параметризованный тип, Self — явный параметр типа)
Animal: (Self: Type) -> Type = {
    speak: (self: &Self) -> String,
}

# Определение типа (внутреннее объявление)
Dog: Type = {
    x: Int = 10,
    Animal(Dog),  # Инстанциирование интерфейса, Self ↦ Dog
    speak: (self: &Dog) -> String = "Woof",
}

# Внешнее объявление (перегрузка)
Dog.speak: (self: &Dog, volume: Int) -> String = "WOOF"

# Гетерогенный контейнер (динамическая диспетчеризация)
animals: List(Animal) = [Dog.new(), Cat.new()]
animals[0].speak()  # "Woof"

Соглашение о записи получателя (receiver) (в связке с семантикой владения из RFC-009):

  • Получатель метода следует семантике сигнатуры: &Self — заимствование (соглашение интерфейса по умолчанию — вызов метода не потребляет получателя), &mut Self — изменяемое заимствование, по значению Self — потребление получателя (Move, RFC-009).
  • Self в сигнатуре на стороне impl — это псевдоним типа impl: интерфейс speak: (self: &Self) сопоставляется с impl (self: &Dog) / (self: &Self) (после подстановки Self ↦ тип impl они полностью совпадают, §3).
  • Историческая запись получателя по значению ((self: Self)) означала заимствование; в настоящем документе она унифицирована в явную форму &Self; запись по значению отныне сохраняет только семантику «потребления» и больше не смешивается.

Устранённая сложность:

  • ❌ Нет ключевого слова impl
  • ❌ Нет магического ключевого слова Self (Self — явный параметр типа, ничем не отличается от T)
  • ❌ Нет аннотации dyn Trait + 'a
  • ❌ Нет виртуальной таблицы (сбор типов на этапе compile-time + обёртка в enum)
  • ❌ Нет переопределения (единые правила перегрузки)

Мотивация ​

Недостатки RFC-011 ​

RFC-011 определяет систему обобщений, но не детализирует:

ВопросОписание
Синтаксис объявления интерфейсаКак объявить, что тип реализует интерфейс?
Расположение реализации методаВнутреннее или внешнее объявление?
Правила перегрузкиКак обрабатывать одноимённые методы?
Синтаксис значений по умолчаниюКак задать полю значение по умолчанию?
Динамическая диспетчеризацияКак реализовать гетерогенный контейнер?

Цели проектирования ​

  1. Простота: ключевое слово impl не требуется
  2. Гибкость: реализация методов поддерживается и внутренне, и внешне
  3. Единообразие: правила перегрузки согласованы
  4. Удобство: лаконичный синтаксис значений по умолчанию
  5. Нулевые накладные расходы: без виртуальной таблицы, сбор типов на этапе compile-time

Сравнение с Rust ​

СвойствоRustYaoXiang
Объявление интерфейсаimpl Animal for Dog { ... }Dog: Type = { Animal(Dog), ... }
Реализация методаВ блоке implВнутренне или внешне
ПерегрузкаНе поддерживаетсяПоддерживается (при разных сигнатурах)
Значение по умолчаниюТребуется #[default]Пишется сразу = value
Гетерогенный контейнерVec<Box<dyn Animal + 'a>>List(Animal)
Динамическая диспетчеризацияПоиск по виртуальной таблицеСбор типов на этапе compile-time
Ключевое слово SelfМагическое, неявно квантифицируемоеЯвный параметр типа, равноправный с T

Предложение ​

1. Объявление интерфейса ​

Основное правило: интерфейс — это параметризованный тип (Self: Type) -> Type, где Self — явный параметр типа, а не магическое ключевое слово. При реализации интерфейс вызывается с передачей конкретного типа.

yaoxiang
# Определение интерфейса (полностью совпадает с обобщёнными типами из RFC-011)
Animal: (Self: Type) -> Type = {
    speak: (self: &Self) -> String,
}

# Объявление типа, реализующего интерфейс
Dog: Type = {
    x: Int,
    Animal(Dog),  # Инстанциирование интерфейса, Self ↦ Dog
}

Обработка компилятором:

  1. Распознать, что Animal(Dog) — это инстанциирование (Self: Type) -> Type
  2. Выполнить подстановку Self ↦ Dog: развернуть Animal(Dog) → { speak: (self: &Dog) -> String }
  3. Проверить, предоставляет ли Dog все требуемые методы (соответствие сигнатур)
  4. Если проверка пройдена → сгенерировать доказательство реализации
  5. Если проверка не пройдена → ошибка компиляции

Эквивалент развёртывания:

yaoxiang
Dog: Type = {
    x: Int,
    Animal(Dog),  # Разворачивается в методы Animal, сохраняя метку источника
}

# Эквивалентно (с сохранённой информацией об источнике)
Dog: Type = {
    x: Int,
    speak: (self: &Dog) -> String,  # Из Animal, Self уже подставлен как Dog
}

Зачем нужна метка источника:

  • Прямое развёртывание теряет информацию об источнике
  • Метка источника используется для генерации доказательства реализации
  • Во время выполнения через доказательство находится корректный метод

1.1 Параметр типа Self и момент проверки типов ​

Self — это явный параметр типа интерфейса, а не магическое ключевое слово. Animal: (Self: Type) -> Type и List: (T: Type) -> Type — это одно и то же — конструктор типа (Type) -> Type.

Момент проверки типов:

  • При определении интерфейса: Self в { speak: (self: &Self) -> String } — абстрактный параметр типа, выполняется только синтаксическая проверка.
  • В точке инстанциирования: при Animal(Dog) выполняется подстановка Self ↦ Dog, после развёртывания производится полная проверка типов (соответствие сигнатур, наличие метода).

Это устраняет проблему Self как неявного магического ключевого слова в RFC-011 — Self не появляется в определении типа, он встречается лишь один раз в списке параметров интерфейса, полностью равноправно с T.

1.2 Пространство имён полей и методов ​

Имена полей и методов типа разделяют одно и то же пространство имён. Если после развёртывания интерфейса имя метода интерфейса конфликтует с именем поля типа, компиляция завершается ошибкой:

yaoxiang
Drawable: (Self: Type) -> Type = {
    x: (self: &Self) -> Int,    # Метод с именем x
}

Point: Type = {
    x: Int,                     # Поле тоже с именем x
    Drawable(Point),            # ❌ Ошибка компиляции: Drawable требует метод x, конфликтующий с полем x
}

Обращение к полю point.x и вызов метода point.x() синтаксически неразличимы. Единое пространство имён снимает неоднозначность.

2. Реализация методов ​

Основное правило: поддерживается как внутреннее, так и внешнее объявление методов.

2.1 Внутреннее объявление ​

yaoxiang
Dog: Type = {
    x: Int = 10,
    Animal(Dog),
    speak: (self: &Dog) -> String = "Woof",  # Реализация метода внутри
}

2.2 Внешнее объявление ​

yaoxiang
Dog: Type = {
    x: Int,
    Animal(Dog),
}

# Реализация метода снаружи
Dog.speak: (self: &Dog) -> String = "Woof"

2.3 Смешанное объявление ​

yaoxiang
Dog: Type = {
    x: Int = 10,
    Animal(Dog),
    speak: (self: &Dog) -> String = "Woof",  # Часть методов внутри
}

# Часть методов снаружи
Dog.play: (self: &Dog) -> Void = { ... }

Обработка компилятором:

  1. Собрать все определения (внутренние и внешние)
  2. Сгруппировать по сигнатурам (перегрузка)
  3. Проверить наличие переопределений (сообщить об ошибке)
  4. Проверить полноту интерфейса
  5. Сгенерировать доказательство реализации

3. Перегрузка и переопределение ​

Основное правило:

  • Разные сигнатуры → перегрузка → разрешена
  • Одинаковые сигнатуры → переопределение → ошибка

3.1 Перегрузка (разрешена) ​

yaoxiang
# Разные типы параметров — перегрузка разрешена
Dog.speak: (self: &Dog) -> String = "Woof"
Dog.speak: (self: &Dog, volume: Int) -> String = "WOOF"

3.2 Переопределение (запрещено) ​

yaoxiang
# Полностью идентичные сигнатуры — переопределение запрещено
Dog.speak: (self: &Dog) -> String = "Woof"
Dog.speak: (self: &Dog) -> String = "Bark"  # ❌ Ошибка: переопределение недопустимо

Сообщение об ошибке:

ошибка: повторное определение Dog.speak(self: &Dog) -> String
  --> файл2:5:1
  |
5 | Dog.speak: (self: &Dog) -> String = "Bark"
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ повторное определение
  |
  --> файл1:3:1
  |
3 | Dog.speak: (self: &Dog) -> String = "Woof"
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ первое определение

3.3 Единые правила ​

Внутренние и внешние объявления подчиняются одним и тем же правилам перегрузки/переопределения:

yaoxiang
# Внутреннее объявление
Dog: Type = {
    x: Int,
    Animal(Dog),
    speak: (self: &Dog) -> String = "Woof",
}

# Внешнее объявление (перегрузка, разрешена)
Dog.speak: (self: &Dog, volume: Int) -> String = "WOOF"

# Внешнее объявление (переопределение, запрещено)
Dog.speak: (self: &Dog) -> String = "Bark"  # ❌ Ошибка

4. Значения по умолчанию ​

Основное правило: после поля сразу пишется = value, что избавляет от необходимости в конструкторе.

yaoxiang
Dog: Type = {
    x: Int = 10,  # Значение по умолчанию
    y: Int = 20,  # Значение по умолчанию
    Animal(Dog),
}

Конструктор, генерируемый компилятором:

yaoxiang
# У всех полей есть значения по умолчанию → генерируется конструктор без аргументов
Dog.new: () -> Dog = { x: 10, y: 20 }

# У части полей есть значения по умолчанию → генерируется конструктор с частичными аргументами
Dog.new: (x: Int) -> Dog = { x: x, y: 20 }
Dog.new: (y: Int) -> Dog = { x: 10, y: y }

# Конструктор со всеми аргументами
Dog.new: (x: Int, y: Int) -> Dog = { x: x, y: y }

Внешнее объявление значений по умолчанию:

yaoxiang
Dog: Type = {
    x: Int,
    y: Int,
    Animal(Dog),
}

# Внешнее объявление значений по умолчанию
Dog.x: Int = 10
Dog.y: Int = 20

Эквивалентно внутреннему объявлению.

5. Реализация в компиляторе ​

5.1 Дескриптор интерфейса ​

rust
// Внутреннее представление компилятора: дескриптор интерфейса
struct InterfaceDescriptor {
    name: String,
    self_param: TypeParam,     // Параметр типа Self
    methods: Vec<MethodSignature>,
}

5.2 Определение типа ​

rust
// Внутреннее представление компилятора: определение типа
struct TypeDefinition {
    name: String,
    fields: Vec<Field>,
    interface_instantiations: Vec<InterfaceInstantiation>,
}

// Инстанциирование интерфейса (Self ↦ ConcreteType)
struct InterfaceInstantiation {
    interface: InterfaceId,
    self_type: TypeId,          // Конкретный тип, которым заменён Self
    methods: HashMap<MethodId, FunctionBody>,
}

5.3 Доказательство реализации ​

rust
// Внутреннее представление компилятора: доказательство реализации
struct ImplementationProof {
    type_id: TypeId,
    interface_id: InterfaceId,
    methods: Vec<MethodPointer>,
}

5.4 Конвейер компиляции ​

1. Разобрать определения типов, собрать объявления инстанциирования интерфейсов (Animal(Dog))
2. Для каждого инстанциирования интерфейса выполнить подстановку Self ↦ ConcreteType
3. Развернуть сигнатуры методов интерфейса, проверить соответствие сигнатур
4. Собрать все определения методов (внутренние и внешние)
5. Сгруппировать по сигнатурам (перегрузка)
6. Проверить переопределения (сообщить об ошибке)
7. Проверить полноту интерфейса
8. Сгенерировать доказательство реализации

6. Динамическая диспетчеризация ​

Основной замысел: сбор типов на этапе compile-time + сопоставление интерфейса, без виртуальной таблицы.

6.1 Гетерогенный контейнер ​

Animal — это (Self: Type) -> Type. List(Animal) использует неинстанциированный конструктор типа интерфейса в качестве экзистенциального типа: ∃S. Animal(S) — «существует некоторый тип S, такой что S реализует Animal(S)».

yaoxiang
# Определение интерфейса
Animal: (Self: Type) -> Type = {
    speak: (self: &Self) -> String,
}

# Определения типов
Dog: Type = {
    x: Int,
    Animal(Dog),
    speak: (self: &Dog) -> String = "Woof",
}

Cat: Type = {
    y: Int,
    Animal(Cat),
    speak: (self: &Cat) -> String = "Meow",
}

# Гетерогенный контейнер — Animal не инстанциирован = экзистенциальный тип
animals: List(Animal) = [Dog.new(), Cat.new()]
animals[0].speak()  # "Woof"
animals[1].speak()  # "Meow"

Семантика владения: помещение в гетерогенный контейнер имеет семантику Move (RFC-009). Dog.new() перемещается в вариант перечисления AnimalGroup::Dog, исходная переменная становится недоступна.

yaoxiang
dog = Dog.new()
animals: List(Animal) = [dog]
# dog.speak()  ← ❌ Ошибка компиляции: dog уже перемещён

6.2 Сбор типов на этапе compile-time ​

Ключевая стратегия: отслеживание владения, инкрементальное построение. Вместо того чтобы сканировать на этапе compile-time все типы, реализующие интерфейс, — инкрементальный сбор происходит в каждой точке операции владения для List(Animal):

yaoxiang
// Точка конструирования
animals: List(Animal) = [Dog.new()]       // AnimalGroup = { Dog(Dog) }

// Точка append
animals.append(Cat.new())                  // Компилятор в точке append видит Cat → расширяет до { Dog, Cat }
animals.append(Bird.new())                 // Ещё раз расширяет { Dog, Cat, Bird }

Обработка компилятором (инкрементально):

  1. При первом конструировании List(Animal) → сгенерировать начальный enum (все типы конструирования, известные в текущей единице компиляции)
  2. На каждом append / push / присваивании по индексу → проверить, присутствует ли тип значения в enum; если нет — расширить вариант enum
  3. Для итогового enum сгенерировать мономорфизированный код диспетчеризации через match
  4. Между единицами компиляции: полагаться на LTO (Link-Time Optimization) для слияния вариантов enum. Когда Animal как экзистенциальный тип передаётся через границу единицы компиляции, каждая единица генерирует частичный набор вариантов, на этапе линковки они сливаются в полный enum.

Автоматически сгенерированный enum:

yaoxiang
# Автоматически генерируется компилятором (невидимо для пользователя)
AnimalGroup: Type = {
    Dog(Dog),
    Cat(Cat),
    Bird(Bird),    # ← Расширено при append(Bird.new())
}

# List(Animal) внутри эквивалентен List(AnimalGroup)

6.3 Проверка соответствия интерфейсу ​

Ключевое наблюдение: проверка соответствия интерфейсу выполняется на этапе compile-time, даже если типы поступают из динамически загружаемых плагинов.

yaoxiang
# Система плагинов
plugin = load_plugin("bird.so")

# Проверка компилятора: тип возврата plugin.create_bird() должен реализовывать Animal
bird: Animal = plugin.create_bird()  # Проверка на этапе compile-time, экзистенциальный тип

# Помещение в гетерогенный контейнер — точка append запускает расширение enum
animals: List(Animal) = [Dog.new(), Cat.new()]
animals.append(bird)                 # Компилятор: (1) проверить, что bird реализует Animal (2) расширить enum

Обработка компилятором:

  1. Проверить тип возврата аргумента append
  2. Проверить, реализует ли этот тип целевой интерфейс
  3. Если проверка пройдена → расширить enum, разрешить помещение
  4. Если проверка не пройдена → ошибка компиляции

6.4 Диспетчеризация во время выполнения ​

Поток вызова (match по enum на этапе compile-time, ImplementationProof уже стёрт):

animals[0].speak()
  ↓
match, сгенерированный компилятором:
  match animals[0] {
    AnimalGroup.Dog(d) => d.speak(),
    AnimalGroup.Cat(c) => c.speak(),
    AnimalGroup.Bird(b) => b.speak(),
  }

Проекция бренда (взаимодействие с RFC-009a): привязка шаблона match AnimalGroup.Dog(d) порождает в дереве брендов дочерний бренд #animals[0].Dog, эквивалентный проекции поля (#42.field_x). Цепочка брендов ReadToken(d), создаваемая d.speak(), имеет вид animals → animals[0] → d → ReadToken(d); проверяющий заимствования верифицирует конфликты по совпадению префиксов в дереве брендов.

Тип при доступ по индексу: animals[0] возвращает &AnimalGroup (сгенерированный компилятором тип enum), пользователь не может напрямую получить &mut Animal. Изменяемый доступ реализуется косвенно через методы интерфейса (например, animals[0].mutate() внутри разворачивается в AnimalGroup::Dog(d) => d.mutate()).

Сравнение с виртуальной таблицей:

Виртуальная таблица (Rust)Enum на этапе compile-time (YaoXiang)
Способ поискаУказатель виртуальной таблицы → указатель на методmatch по enum → прямой вызов
Накладные расходы во время выполненияОдно косвенное обращениеВетвление (оптимизируется предсказанием переходов CPU)
Генерация на этапе compile-timeВиртуальная таблицаEnum + match
Аннотации пользователяТребуется dyn Trait + 'aНе требуются
ImplementationProofНеприменимоСтирается на этапе compile-time, отсутствует во время выполнения

Преимущества YaoXiang:

  • Не требуется аннотация бренда
  • Типобезопасность на этапе compile-time
  • Прозрачно для пользователя (не нужно писать dyn Animal)
  • ImplementationProof — чисто compile-time концепция, нулевые накладные расходы во время выполнения

6.5 Ограничения и область применения ​

В рамках одной единицы компиляции: полная поддержка. Отслеживание владения покрывает все точки append/конструирования, enum строится инкрементально.

Между единицами компиляции: полагается на LTO (Link-Time Optimization) для слияния вариантов enum. Animal передаётся через границу единиц компиляции как экзистенциальный тип (∃S. Animal(S)). Каждая единица генерирует частичный набор вариантов, на этапе линковки они сливаются.

Не поддерживается: полностью динамические типы во время выполнения (полностью утиная типизация). Множество типов должно быть полностью известно на этапе compile-time.

6.6 Примечания к реализации (фаза 3, реализовано в v1) ​

Семантика §6 (гетерогенный контейнер, проверка членов на этапе compile-time, диспетчеризация по фактическому типу, замкнутость множества типов на этапе compile-time) полностью реализована. Форма реализации конкретизирована на уровне механизмов следующим образом:

  • Сбор типов: вместо «инкрементального сбора по точкам операций владения» из §6.2 выполняется одноразовый сбор по ImplementationProof для всей единицы компиляции. В пределах одной единицы компиляции оба подхода семантически эквивалентны (лишние мёртвые варианты безвредны); ценность инкрементального сбора проявляется в межъединичных сценариях, которые отнесены к v2 (см. ниже).
  • Представление: компилятор синтезирует вариантный тип Animal$Group — чистый IR/байткод/артефакт среды выполнения (инструкции CreateVariant/VariantTag/VariantPayload, значение среды выполнения RuntimeValue::Enum), MonoType ничего о нём не знает — на уровне проверки типов пользователь по-прежнему видит имя интерфейса. Каждое конкретное значение, попадающее в позицию экзистенциального типа, автоматически оборачивается в значение варианта (единое непрозрачное представление, семантика §6.4).
  • Точки обёртывания: проверка типов выполняет направленный обход в местах, где принимается решение «конкретный vs экзистенциальный» (аннотированный let/аргумент вызова/return/элементы литерала списка), порождая таблицу принудительной обёртки по span'ам. Генерация IR внедряет обёртывание по span'ам. Пропущенная обёртка отвергается защитным механизмом среды выполнения с громким сигналом (проверки VariantTag/VariantPayload требуют, чтобы значение было вариантом именованной группы), в худшем случае — явная ошибка во время выполнения на этапе тестирования, никогда не производит ошибочные данные молча.
  • Диспетчеризация: цепочка переходов по сравнению номера варианта, в каждой ветви распаковывается полезная нагрузка и статически вызывается конкретный метод; форма перепривязки из RFC-004 (Type.method = fn[n]) после переупорядочения по позиции привязки также участвует в диспетчеризации.
  • Изоляция: устаревшие ограничения trait (в форме Drawable: Type = {..}, без параметров обобщений) не проходят через диспетчеризацию по вариантам, поведение не меняется.

Границы v1 (последующие фазы): межъединичное слияние вариантов через LTO (§6.5); сопоставление с образцом значений Group (зависит от IR-поддержки образцов вариантов в match); взаимодействие с рефлексией; семантика Move в контейнер; поток-посредник через Any/переменные типа и границы лямбд с выводом типов (аварийный предохранитель — защита во время выполнения).


Анализ вариантов использования ​

Базовая реализация интерфейса ​

yaoxiang
# Определение интерфейса
Animal: (Self: Type) -> Type = {
    speak: (self: &Self) -> String,
}

# Определение типа
Dog: Type = {
    x: Int = 10,
    Animal(Dog),
    speak: (self: &Dog) -> String = "Woof",
}

# Использование
dog = Dog.new()
dog.speak()  # "Woof"

Реализация нескольких интерфейсов ​

yaoxiang
# Несколько интерфейсов
Animal: (Self: Type) -> Type = {
    speak: (self: &Self) -> String,
}

Pet: (Self: Type) -> Type = {
    name: (self: &Self) -> String,
}

# Тип реализует несколько интерфейсов
Dog: Type = {
    x: Int = 10,
    Animal(Dog),
    Pet(Dog),
    speak: (self: &Dog) -> String = "Woof",
    name: (self: &Dog) -> String = "Buddy",
}

# Использование
dog = Dog.new()
dog.speak()  # "Woof"
dog.name()   # "Buddy"

Обобщённый интерфейс ​

yaoxiang
# Обобщённый интерфейс
Container: (Self: Type, T: Type) -> Type = {
    add: (self: &mut Self, item: T) -> Void,
    get: (self: &Self, index: Int) -> T,
}

# Реализация обобщённого интерфейса
IntList: Type = {
    data: Array(Int),
    Container(IntList, Int),
    add: (self: &mut IntList, item: Int) -> Void = ...,
    get: (self: &IntList, index: Int) -> Int = ...,
}

Гетерогенный контейнер ​

yaoxiang
# Определение интерфейса
Animal: (Self: Type) -> Type = {
    speak: (self: &Self) -> String,
}

# Определения типов
Dog: Type = {
    x: Int,
    Animal(Dog),
    speak: (self: &Dog) -> String = "Woof",
}

Cat: Type = {
    y: Int,
    Animal(Cat),
    speak: (self: &Cat) -> String = "Meow",
}

# Гетерогенный контейнер
animals: List(Animal) = [Dog.new(), Cat.new()]

# Использование
for animal in animals {
    print(animal.speak())
}
# Вывод:
# Woof
# Meow

Система плагинов ​

yaoxiang
# Определение интерфейса
Plugin: (Self: Type) -> Type = {
    name: (self: &Self) -> String,
    execute: (self: &Self) -> Void,
}

# Основная программа
main: () -> Void = {
    # Загрузка плагинов
    plugin1 = load_plugin("plugin1.so")
    plugin2 = load_plugin("plugin2.so")

    # Проверка компилятора: plugin1 и plugin2 должны реализовывать интерфейс Plugin
    plugins: List(Plugin) = [plugin1, plugin2]

    # Выполнение всех плагинов
    for plugin in plugins {
        print(plugin.name())
        plugin.execute()
    }
}

Компромиссы ​

Преимущества ​

  1. Простота: ключевое слово impl не требуется
  2. Гибкость: реализация методов поддерживается и внутренне, и внешне
  3. Единообразие: правила перегрузки согласованы
  4. Удобство: лаконичный синтаксис значений по умолчанию
  5. Нулевые накладные расходы: без виртуальной таблицы, сбор типов на этапе compile-time
  6. Типобезопасность: проверка соответствия интерфейсу на этапе compile-time
  7. Прозрачность для пользователя: не нужно писать dyn Animal + 'a

Недостатки ​

  1. Ограничение: не поддерживаются полностью динамические типы во время выполнения (полностью утиная типизация)
  2. Затраты на этапе компиляции: для каждого интерфейса необходимо сгенерировать варианты enum и код диспетчеризации через match
  3. Множество типов: должно быть полностью известно на этапе compile-time (в пределах одной единицы компиляции)

Меры по смягчению ​

  1. Система плагинов: поддерживается через проверку соответствия интерфейсу на этапе compile-time
  2. Множество типов: отслеживание владения, инкрементальное построение — сбор в каждой точке append/конструирования, а не глобальное сканирование
  3. Между единицами компиляции: слияние наборов вариантов enum на этапе линковки, общий механизм с мономорфизацией во время линковки

Альтернативы ​

АльтернативаПочему не выбрана
Ключевое слово implУвеличивает сложность синтаксиса
Виртуальная таблица (dyn Trait)Требуется аннотация бренда ('a)
Полностью утиная типизацияНакладные расходы во время выполнения, отсутствие типобезопасности
Обёртка в enum (вручную)Высокая нагрузка на пользователя

Связь с RFC-009 ​

Бренд и реализация интерфейса:

  • Реализация интерфейса — на уровне типов, не затрагивает бренды
  • Бренды — на уровне доказательства заимствования (RFC-009a)
  • Эти два аспекта ортогональны и не влияют друг на друга

Динамическая диспетчеризация и бренды:

  • Динамическая диспетчеризация использует доказательство реализации, не требует аннотации бренда
  • Доказательство реализации генерируется на этапе compile-time, нулевой поиск во время выполнения
  • Позволяет избежать сложности dyn Trait + 'a

Владение для гетерогенного контейнера:

  • Помещение в List(Animal) имеет семантику Move (RFC-009), исходная переменная становится недоступна
  • Доступ по индексу animals[0] возвращает &AnimalGroup (сгенерированный компилятором enum), цепочка проекции бренда: animals → animals[0] → enum_variant → field
  • Изменяемый доступ реализуется косвенно через методы интерфейса, не предоставляя пользователю &mut AnimalGroup

Наследование интерфейсов ​

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

yaoxiang
Animal: (Self: Type) -> Type = {
    speak: (self: &Self) -> String,
}

Pet: (Self: Type) -> Type = {
    Animal(Self),                       # Pet наследует Animal — никаких новых ключевых слов
    name: (self: &Self) -> String,
}

# Когда Dog реализует Pet, он должен одновременно удовлетворять всем методам и Animal, и Pet
Dog: Type = {
    x: Int,
    Pet(Dog),
    speak: (self: &Dog) -> String = "Woof",  # Из Animal
    name: (self: &Dog) -> String = "Buddy",  # Из Pet
}

Принцип проектирования: Наследование существует, но его злоупотребление не поощряется. Основной способ композиции — инстанциирование нескольких интерфейсов (Dog: Type = { Animal(Dog), Pet(Dog), ... }). Тип может напрямую объявить все интерфейсы, которым он удовлетворяет, без построения дерева наследования. Наследование интерфейсов применяется только при наличии явной иерархии «is-a».

Обработка компилятором: разворачивает цепочку наследования. Pet(Self) разворачивается в { все методы Animal(Self), name: ... }. Когда Dog объявляет Pet(Dog), выполняется Self ↦ Dog, компилятор проверяет, что Dog одновременно удовлетворяет всем методам Animal(Dog) и Pet(Dog).

Подстановка Self при наследовании интерфейсов: в Pet: (Self: Type) -> Type = { Animal(Self), ... } Self в Animal(Self) — это параметр Self интерфейса Pet — он подставляется отложенно. Когда Dog реализует Pet(Dog), выполняется Self ↦ Dog, Animal(Self) превращается в Animal(Dog). Это полностью соответствует семантике передачи параметров в обобщённых функциях.

Реализация методов по умолчанию ​

Интерфейс может предоставлять реализацию метода по умолчанию. Реализующий тип может переопределить её или унаследовать:

yaoxiang
fmt: (Self: Type) -> Type = {
    display: (self: &Self) -> String,                      # Должен быть реализован
    debug: (self: &Self) -> String = self.display(),       # ✅ Ссылается на метод того же интерфейса
    summary: (self: &Self) -> String = f"<{self.name}>",  # ❌ Ошибка компиляции: self.name не в fmt
}

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

Наследование может предполагать реализацию нижестоящего уровня: когда интерфейс Pet(Self) наследует Animal(Self), методы по умолчанию Pet могут использовать методы, объявленные в Animal — ведь они унаследованы, следовательно, гарантированно существуют.

yaoxiang
Animal: (Self: Type) -> Type = {
    speak: (self: &Self) -> String,
}

Pet: (Self: Type) -> Type = {
    Animal(Self),                                              # Наследование
    name: (self: &Self) -> String,
    introduce: (self: &Self) -> String = self.name() + " says " + self.speak(),  # ✅ speak приходит из унаследованного Animal
}

Поведение на этапе compile-time: когда тип реализует интерфейс, для каждого метода:

  1. Тип предоставил метод → используется метод типа
  2. Тип не предоставил метод, у интерфейса есть реализация по умолчанию → компилятор встраивает реализацию по умолчанию в тип (нулевые накладные расходы виртуальной таблицы)
  3. Тип не предоставил метод, у интерфейса нет реализации по умолчанию → ошибка компиляции

Принцип проектирования: метод по умолчанию подобен механизму автоматического вывода Copy/Clone — компилятор при необходимости генерирует его автоматически, пользователь может переопределить. Ключевые слова virtual/override/super не вводятся. ​

Фазы реализации ​

ФазаСодержаниеЗависимость
Фаза 1Синтаксис объявления интерфейса ((Self: Type) -> Type) + параметр типа SelfRFC-011
Фаза 2Инстанциирование интерфейса (Animal(Dog)) + подстановка Self ↦ ConcreteTypeФаза 1
Фаза 3Внутреннее/внешнее объявление реализации методаФаза 2
Фаза 4Правила перегрузки и переопределенияФаза 3
Фаза 5Синтаксис значений по умолчаниюФаза 3
Фаза 6Наследование интерфейсовФаза 4
Фаза 7Реализация методов по умолчаниюФаза 6
Фаза 8Генерация доказательства реализацииФаза 7
Фаза 9Сбор типов на этапе compile-timeФаза 8
Фаза 10Реализация динамической диспетчеризацииФаза 9

Записи о проектных решениях ​

РешениеОпределениеПричинаДата
Синтаксис объявления интерфейсаИнтерфейс — параметризованный тип (Self: Type) -> Type, инстанциируется при реализацииУстраняет магическое ключевое слово Self, полностью согласуется с системой обобщений RFC-0112026-06-14
Параметр типа SelfЯвный параметр типа, при определении интерфейса — только синтаксическая проверка, полная проверка — в точке инстанциированияИзбежание свободных переменных типа в HM-выводе2026-06-14
Динамическая диспетчеризацияСбор типов на этапе compile-time + автоматическая генерация enumБез виртуальной таблицы, нулевой поиск во время выполнения, прозрачно для пользователя2026-06-14
Внешнее объявление методаПоддерживаетсяРавная гибкость с внутренним объявлением, компилятор отвечает за сбор по файлам2026-06-14
ПереопределениеЗапрещено (ошибка при одинаковой сигнатуре)Переопределение ведёт к непредсказуемому поведению, перегрузка покрывает все случаи2026-06-14
Наследование интерфейсовПоддерживается, без нового синтаксисаТа же синтаксическая позиция, что и при объявлении типа, реализующего интерфейс. Поощряется композиция (инстанциирование нескольких интерфейсов), не поощряется глубокое дерево наследования2026-07-03
Реализация методов по умолчаниюПоддерживается, подобно автоматическому выводу Copy/CloneИнтерфейс предоставляет тело, компилятор при необходимости встраивает; пользователь может переопределить. Ключевые слова virtual/override не вводятся2026-07-03
Ограничения метода по умолчаниюПроверка в момент определения интерфейса: может ссылаться только на методы того же интерфейса, не может предполагать реализацию вышестоящего уровняИнтерфейс — замкнутый контракт. Наследование может предполагать реализацию нижестоящего уровня, но интерфейс не может предполагать поля/методы реализующего типа2026-07-03
Стратегия сбора типовОтслеживание владения, инкрементальное построение — сбор в каждой точке append/конструированияНе глобальное сканирование всех реализаторов, а инкрементальное расширение enum по точкам операций владения2026-07-03
ImplementationProofЧисто compile-time концепция, стирается во время выполненияВо время выполнения — диспетчеризация через match по enum, доказательство используется только для проверки на этапе compile-time2026-07-03
Между единицами компиляцииLTO для слияния вариантов enumЭкзистенциальный тип передаётся через границу единиц компиляции, каждая единица генерирует частичный enum, слияние на этапе LTO2026-07-03
Пространство имён полей/методовЕдиное пространство имён, конфликт — ошибкаОбращение к полю point.x и вызов метода point.x() синтаксически неразличимы, единое пространство снимает неоднозначность2026-07-03
Владение в гетерогенном контейнереСемантика Move, после помещения в контейнер исходная переменная недоступнаСогласуется с моделью владения из RFC-0092026-07-03
Проекция брендаПривязка шаблона match порождает дочерний бренд, эквивалентный проекции поляСогласуется с механизмом дерева брендов из RFC-009a, проекция варианта enum — допустимый путь в дереве брендов2026-07-03
Соглашение о записи получателя&Self — заимствование / &mut Self — изменяемое заимствование / по значению — MoveПолучатель следует семантике сигнатуры (RFC-009), интерфейс по умолчанию — заимствование; историческая запись по значению мигрирует в &Self2026-08-30

Открытые вопросы ​

  • [x] Наследование интерфейсов (интерфейс может наследовать другие интерфейсы) → Поддерживается, без нового синтаксиса. Pet: (Self: Type) -> Type = { Animal(Self), ... }
  • [x] Реализация методов по умолчанию (интерфейс может предоставлять реализацию по умолчанию) → Поддерживается, подобно автоматическому выводу Copy. Интерфейс предоставляет тело, компилятор при необходимости встраивает
  • [x] Self как неявное магическое ключевое слово → Устранено. Self — явный параметр типа, интерфейс — это (Self: Type) -> Type
  • [ ] Продвинутые варианты ограничений интерфейса (ассоциированные типы, GAT) — ассоциированные типы реализуются через параметры обобщённого интерфейса (Container: (Self: Type, T: Type) -> Type), GAT требует дальнейшего проектирования
  • [ ] Взаимодействие с замыканиями (замыкания, реализующие интерфейс) — начальная стратегия: замыкания не поддерживают прямую реализацию интерфейса, требуется тип-обёртка. Реализация интерфейса анонимными типами — в следующих RFC

Ссылки ​


Жизненный цикл и итоговое расположение ​

СтатусРасположениеОписание
Принятоdocs/design/rfc/accepted/Официальный проектный документ