RFC-011a: Реализация интерфейсов и динамическая диспетчеризация
Родительский RFC: RFC-011: Проектирование системы обобщений (generics)
Настоящий RFC дополняет и заменяет разделы §2.1-2.4 RFC-011, посвящённые ограничениям интерфейса.
Краткое содержание
RFC-011 определяет систему обобщений, но не детализирует механизм реализации интерфейсов. Настоящий документ дополняет:
- Объявление интерфейса: интерфейс — это параметризованный тип —
(Self: Type) -> Type, при реализации в него передаётся конкретный тип - Реализация методов: поддерживается как внутреннее, так и внешнее объявление
- Правила перегрузки: разные сигнатуры разрешают перегрузку, одинаковые сигнатуры приводят к ошибке (переопределение запрещено)
- Значения по умолчанию: после поля сразу пишется
= value - Динамическая диспетчеризация: сбор типов на этапе compile-time + сопоставление интерфейса, без виртуальной таблицы
Основной замысел:
# Определение интерфейса (параметризованный тип, 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 определяет систему обобщений, но не детализирует:
| Вопрос | Описание |
|---|---|
| Синтаксис объявления интерфейса | Как объявить, что тип реализует интерфейс? |
| Расположение реализации метода | Внутреннее или внешнее объявление? |
| Правила перегрузки | Как обрабатывать одноимённые методы? |
| Синтаксис значений по умолчанию | Как задать полю значение по умолчанию? |
| Динамическая диспетчеризация | Как реализовать гетерогенный контейнер? |
Цели проектирования
- Простота: ключевое слово
implне требуется - Гибкость: реализация методов поддерживается и внутренне, и внешне
- Единообразие: правила перегрузки согласованы
- Удобство: лаконичный синтаксис значений по умолчанию
- Нулевые накладные расходы: без виртуальной таблицы, сбор типов на этапе compile-time
Сравнение с Rust
| Свойство | Rust | YaoXiang |
|---|---|---|
| Объявление интерфейса | 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 — явный параметр типа, а не магическое ключевое слово. При реализации интерфейс вызывается с передачей конкретного типа.
# Определение интерфейса (полностью совпадает с обобщёнными типами из RFC-011)
Animal: (Self: Type) -> Type = {
speak: (self: &Self) -> String,
}
# Объявление типа, реализующего интерфейс
Dog: Type = {
x: Int,
Animal(Dog), # Инстанциирование интерфейса, Self ↦ Dog
}Обработка компилятором:
- Распознать, что
Animal(Dog)— это инстанциирование(Self: Type) -> Type - Выполнить подстановку
Self ↦ Dog: развернутьAnimal(Dog)→{ speak: (self: &Dog) -> String } - Проверить, предоставляет ли
Dogвсе требуемые методы (соответствие сигнатур) - Если проверка пройдена → сгенерировать доказательство реализации
- Если проверка не пройдена → ошибка компиляции
Эквивалент развёртывания:
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 Пространство имён полей и методов
Имена полей и методов типа разделяют одно и то же пространство имён. Если после развёртывания интерфейса имя метода интерфейса конфликтует с именем поля типа, компиляция завершается ошибкой:
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 Внутреннее объявление
Dog: Type = {
x: Int = 10,
Animal(Dog),
speak: (self: &Dog) -> String = "Woof", # Реализация метода внутри
}2.2 Внешнее объявление
Dog: Type = {
x: Int,
Animal(Dog),
}
# Реализация метода снаружи
Dog.speak: (self: &Dog) -> String = "Woof"2.3 Смешанное объявление
Dog: Type = {
x: Int = 10,
Animal(Dog),
speak: (self: &Dog) -> String = "Woof", # Часть методов внутри
}
# Часть методов снаружи
Dog.play: (self: &Dog) -> Void = { ... }Обработка компилятором:
- Собрать все определения (внутренние и внешние)
- Сгруппировать по сигнатурам (перегрузка)
- Проверить наличие переопределений (сообщить об ошибке)
- Проверить полноту интерфейса
- Сгенерировать доказательство реализации
3. Перегрузка и переопределение
Основное правило:
- Разные сигнатуры → перегрузка → разрешена
- Одинаковые сигнатуры → переопределение → ошибка
3.1 Перегрузка (разрешена)
# Разные типы параметров — перегрузка разрешена
Dog.speak: (self: &Dog) -> String = "Woof"
Dog.speak: (self: &Dog, volume: Int) -> String = "WOOF"3.2 Переопределение (запрещено)
# Полностью идентичные сигнатуры — переопределение запрещено
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 Единые правила
Внутренние и внешние объявления подчиняются одним и тем же правилам перегрузки/переопределения:
# Внутреннее объявление
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, что избавляет от необходимости в конструкторе.
Dog: Type = {
x: Int = 10, # Значение по умолчанию
y: Int = 20, # Значение по умолчанию
Animal(Dog),
}Конструктор, генерируемый компилятором:
# У всех полей есть значения по умолчанию → генерируется конструктор без аргументов
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 }Внешнее объявление значений по умолчанию:
Dog: Type = {
x: Int,
y: Int,
Animal(Dog),
}
# Внешнее объявление значений по умолчанию
Dog.x: Int = 10
Dog.y: Int = 20Эквивалентно внутреннему объявлению.
5. Реализация в компиляторе
5.1 Дескриптор интерфейса
// Внутреннее представление компилятора: дескриптор интерфейса
struct InterfaceDescriptor {
name: String,
self_param: TypeParam, // Параметр типа Self
methods: Vec<MethodSignature>,
}5.2 Определение типа
// Внутреннее представление компилятора: определение типа
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 Доказательство реализации
// Внутреннее представление компилятора: доказательство реализации
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)».
# Определение интерфейса
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, исходная переменная становится недоступна.
dog = Dog.new()
animals: List(Animal) = [dog]
# dog.speak() ← ❌ Ошибка компиляции: dog уже перемещён6.2 Сбор типов на этапе compile-time
Ключевая стратегия: отслеживание владения, инкрементальное построение. Вместо того чтобы сканировать на этапе compile-time все типы, реализующие интерфейс, — инкрементальный сбор происходит в каждой точке операции владения для List(Animal):
// Точка конструирования
animals: List(Animal) = [Dog.new()] // AnimalGroup = { Dog(Dog) }
// Точка append
animals.append(Cat.new()) // Компилятор в точке append видит Cat → расширяет до { Dog, Cat }
animals.append(Bird.new()) // Ещё раз расширяет { Dog, Cat, Bird }Обработка компилятором (инкрементально):
- При первом конструировании
List(Animal)→ сгенерировать начальный enum (все типы конструирования, известные в текущей единице компиляции) - На каждом
append/push/ присваивании по индексу → проверить, присутствует ли тип значения в enum; если нет — расширить вариант enum - Для итогового enum сгенерировать мономорфизированный код диспетчеризации через
match - Между единицами компиляции: полагаться на LTO (Link-Time Optimization) для слияния вариантов enum. Когда
Animalкак экзистенциальный тип передаётся через границу единицы компиляции, каждая единица генерирует частичный набор вариантов, на этапе линковки они сливаются в полный enum.
Автоматически сгенерированный enum:
# Автоматически генерируется компилятором (невидимо для пользователя)
AnimalGroup: Type = {
Dog(Dog),
Cat(Cat),
Bird(Bird), # ← Расширено при append(Bird.new())
}
# List(Animal) внутри эквивалентен List(AnimalGroup)6.3 Проверка соответствия интерфейсу
Ключевое наблюдение: проверка соответствия интерфейсу выполняется на этапе compile-time, даже если типы поступают из динамически загружаемых плагинов.
# Система плагинов
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Обработка компилятором:
- Проверить тип возврата аргумента
append - Проверить, реализует ли этот тип целевой интерфейс
- Если проверка пройдена → расширить enum, разрешить помещение
- Если проверка не пройдена → ошибка компиляции
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/переменные типа и границы лямбд с выводом типов (аварийный предохранитель — защита во время выполнения).
Анализ вариантов использования
Базовая реализация интерфейса
# Определение интерфейса
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"Реализация нескольких интерфейсов
# Несколько интерфейсов
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"Обобщённый интерфейс
# Обобщённый интерфейс
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 = ...,
}Гетерогенный контейнер
# Определение интерфейса
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Система плагинов
# Определение интерфейса
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()
}
}Компромиссы
Преимущества
- Простота: ключевое слово
implне требуется - Гибкость: реализация методов поддерживается и внутренне, и внешне
- Единообразие: правила перегрузки согласованы
- Удобство: лаконичный синтаксис значений по умолчанию
- Нулевые накладные расходы: без виртуальной таблицы, сбор типов на этапе compile-time
- Типобезопасность: проверка соответствия интерфейсу на этапе compile-time
- Прозрачность для пользователя: не нужно писать
dyn Animal + 'a
Недостатки
- Ограничение: не поддерживаются полностью динамические типы во время выполнения (полностью утиная типизация)
- Затраты на этапе компиляции: для каждого интерфейса необходимо сгенерировать варианты enum и код диспетчеризации через match
- Множество типов: должно быть полностью известно на этапе compile-time (в пределах одной единицы компиляции)
Меры по смягчению
- Система плагинов: поддерживается через проверку соответствия интерфейсу на этапе compile-time
- Множество типов: отслеживание владения, инкрементальное построение — сбор в каждой точке
append/конструирования, а не глобальное сканирование - Между единицами компиляции: слияние наборов вариантов 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
Наследование интерфейсов
Интерфейс может включать другой интерфейс. Новый синтаксис не вводится — используется та же синтаксическая позиция, что и при объявлении типа, реализующего интерфейс:
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). Это полностью соответствует семантике передачи параметров в обобщённых функциях.
Реализация методов по умолчанию
Интерфейс может предоставлять реализацию метода по умолчанию. Реализующий тип может переопределить её или унаследовать:
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 — ведь они унаследованы, следовательно, гарантированно существуют.
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: когда тип реализует интерфейс, для каждого метода:
- Тип предоставил метод → используется метод типа
- Тип не предоставил метод, у интерфейса есть реализация по умолчанию → компилятор встраивает реализацию по умолчанию в тип (нулевые накладные расходы виртуальной таблицы)
- Тип не предоставил метод, у интерфейса нет реализации по умолчанию → ошибка компиляции
Принцип проектирования: метод по умолчанию подобен механизму автоматического вывода Copy/Clone — компилятор при необходимости генерирует его автоматически, пользователь может переопределить. Ключевые слова virtual/override/super не вводятся.
Фазы реализации
| Фаза | Содержание | Зависимость |
|---|---|---|
| Фаза 1 | Синтаксис объявления интерфейса ((Self: Type) -> Type) + параметр типа Self | RFC-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-011 | 2026-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-time | 2026-07-03 |
| Между единицами компиляции | LTO для слияния вариантов enum | Экзистенциальный тип передаётся через границу единиц компиляции, каждая единица генерирует частичный enum, слияние на этапе LTO | 2026-07-03 |
| Пространство имён полей/методов | Единое пространство имён, конфликт — ошибка | Обращение к полю point.x и вызов метода point.x() синтаксически неразличимы, единое пространство снимает неоднозначность | 2026-07-03 |
| Владение в гетерогенном контейнере | Семантика Move, после помещения в контейнер исходная переменная недоступна | Согласуется с моделью владения из RFC-009 | 2026-07-03 |
| Проекция бренда | Привязка шаблона match порождает дочерний бренд, эквивалентный проекции поля | Согласуется с механизмом дерева брендов из RFC-009a, проекция варианта enum — допустимый путь в дереве брендов | 2026-07-03 |
| Соглашение о записи получателя | &Self — заимствование / &mut Self — изменяемое заимствование / по значению — Move | Получатель следует семантике сигнатуры (RFC-009), интерфейс по умолчанию — заимствование; историческая запись по значению мигрирует в &Self | 2026-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
Ссылки
- RFC-011: Проектирование системы обобщений (generics) — Родительский RFC
- RFC-009: Проектирование модели владения — Система владения
- RFC-009a: Конвейер доказательства заимствования — Механизм брендов
- RFC-010: Унифицированный синтаксис типов — Унифицированный синтаксис
Жизненный цикл и итоговое расположение
| Статус | Расположение | Описание |
|---|---|---|
| Принято | docs/design/rfc/accepted/ | Официальный проектный документ |
