RFC-010: Унифицированный синтаксис типов - модель name: type = value
Аннотация
Настоящий RFC предлагает предельно минималистичную унифицированную модель синтаксиса типов: всё есть name: type = value.
В YaoXiang существует только одна форма объявления:
identifier : type = expressionгде type может быть произвольным тип-выражением, а expression — произвольным выражением-значением. Нет fn, нет struct, нет trait, нет impl, нет ключевого слова type в нижнем регистре (но есть Type как ключевое слово метатипа).
Основной дизайн:
Typeсам по себе является обобщённым типом.(T: Type) -> Typeозначает «тип, принимающий параметр-тип T».
| Концепция | Код |
|---|---|
| Переменная | x: Int = 42 |
| Функция | add: (a: Int, b: Int) -> Int = a + b |
| Record type | Point: Type = { x: Float, y: Float } |
| Интерфейс | Drawable: Type = { draw: (Surface) -> Void } |
| Обобщённый тип | List: (T: Type) -> Type = { data: Array(T), length: Int } |
| Обобщённый тип | Map: (K: Type, V: Type) -> Type = { keys: Array(K), values: Array(V) } |
| Метод | Point.draw: (p: Point, s: Surface) -> Void = ...Point.draw = draw[0] |
| Обобщённая функция | map: (T: Type, R: Type) -> ((list: List(T), f: (x: T) -> R) -> List(R)) |
Type является единственным ключевым словом метатипа в языке.
Пространство имён vs привязка методов:
Type.nameПрефикс означает принадлежность к пространству имён, и только. Он не вызывает никакой неявной привязки. Чтобы синтаксис.вызоваp.draw(screen)заработал, необходимо явно выполнить привязку:Point.draw = draw[0]. Подробности см. в разделе «Пространство имён и привязка методов» ниже. Используется для обозначения иерархии типов; компилятор автоматически обрабатывает различие между Type0, Type1, Type2..., что прозрачно для пользователя.
// Основной синтаксис: унификация + различие
// Переменная
x: Int = 42
// Функция (имена параметров в сигнатуре)
add: (a: Int, b: Int) -> Int = a + b
// Record type
Point: Type = {
x: Float,
y: Float,
draw: (Surface) -> Void,
serialize: () -> String
}
// Интерфейс (по сути это record type, все поля которого являются функциями)
Drawable: Type = {
draw: (Surface) -> Void,
bounding_box: () -> Rect
}
Serializable: Type = {
serialize: () -> String
}
// Определение метода (используя синтаксис Type.method)
Point.draw: (self: Point, surface: Surface) -> Void = {
surface.plot(self.x, self.y)
}
Point.serialize: (self: Point) -> String = {
return "Point(${self.x}, ${self.y})"
}
// Обобщённый тип ((T: Type) -> Type = обобщённый тип, принимающий параметр-тип)
List: (T: Type) -> Type = {
data: Array(T),
length: Int
}
Map: (K: Type, V: Type) -> Type = {
keys: Array(K),
values: Array(V)
}
// Использование
p: Point = Point(1.0, 2.0)
p.draw(screen) // Синтаксический сахар → Point.draw(p, screen)
s: Drawable = p // Структурный подтип: Point реализует Drawable
drawables: List(Drawable) = [p, r]
process_all(drawables)Мотивация
Зачем нужен этот функционал?
Текущая система типов содержит несколько разрозненных концепций:
- Синтаксис объявления переменных
- Синтаксис определения функций
- Синтаксис определения типов (различные формы)
- Синтаксис определения интерфейсов
- Синтаксис привязки методов
Между этими концепциями отсутствует единство, что приводит к фрагментации синтаксиса и высокой стоимости обучения.
Цели проектирования
- Предельная унификация: одно синтаксическое правило охватывает все случаи
- Лаконичность и элегантность: симметричная эстетика
name: type = value - Без новых ключевых слов: повторное использование существующих синтаксических элементов
- Теоретическая элегантность: типы сами по себе являются значениями типа Type
- Дружелюбие к обобщениям: бесшовная интеграция с системой обобщений (RFC-011)
Интеграция с системой обобщений
Унифицированная модель синтаксиса RFC-010 естественно согласуется с системой обобщений RFC-011; параметры обобщений могут бесшовно интегрироваться в унифицированную модель:
// Базовые обобщения (RFC-011 Phase 1)
List: (T: Type) -> Type = { data: Array(T), length: Int }
// Обобщённые функции (RFC-023 синтаксис: позиция Type в сигнатуре может быть опущена, выводится автоматически при вызове)
map: (: Type, R: Type) -> (( list: List(T), f: (T) -> R) -> List(R)) = ...
// Ограничения типов (RFC-011 Phase 2)
clone: (value: T) -> T = value.clone() // Ограничение T: Clone переносится типом параметра
// Const-обобщения (RFC-011 Phase 4)
Array: (T: Type, N: Int) -> Type = { data: Array(T, N), length: N }Зависимости:
- RFC-011 Phase 1 (базовые обобщения) является сильной зависимостью RFC-010
- Без базовых обобщений обобщённые примеры RFC-010 не скомпилируются
- Рекомендация: RFC-011 Phase 1 и RFC-010 реализовать синхронно
Предложение
Основной принцип: конструктор типов vs функции/переменные
Это ключевой выбор дизайна, определяющий правила разрешения неоднозначностей синтаксиса:
| Запись | Значение | Правило |
|---|---|---|
x: Type = ... | Конструктор типов | : Type явно объявлен → принудительно тип |
f = ... | Функция или переменная | Нет : Type → HM активно выводит как функцию/переменную |
Почему такой дизайн?
Синтаксис { ... } сам по себе неоднозначен:
{ x: Float, y: Float }может быть литералом типа (record type){ a = 1 + 1 }может быть блоком кода (исполняемый оператор, возвращает Void)
Правила разрешения неоднозначностей:
- Есть
: Type→ принудительное解析为类型构造器,{ ... }является литералом типа - Нет
: Type→ HM активно解析为代码块{ ... }, выводит как функциональный тип
# ✅ Конструктор типов: есть : Type
Point: Type = { x: Float, y: Float }
# ✅ Функция: нет : Type, HM выводит как () -> Void
main = { println("Hello") }
# ❌ Ошибка: нет : Type, компилятор не может解析ровать `{ ... }` как тип
Point = { x: Float, y: Float } // HM выводит как функцию, не как тип!Унифицированная модель: identifier : type = expression
├── Переменная
│ └── x: Int = 42
│
├── Функция
│ └── add: (a: Int, b: Int) -> Int = a + b # Нет : Type, HM выводит как функцию
│
├── Record type
│ └── Point: Type = { x: Float, y: Float } # Обязательно возвращает: Type
│
├── Интерфейс
│ └── Drawable: Type = { draw: (Surface) -> Void } # Обязательно возвращает: Type
│
├── Обобщённый тип
│ └── List: (T: Type) -> Type = { data: Array(T), length: Int } # Обязательно возвращает: Type
│
├── Обобщённый тип (несколько параметров)
│ └── Map: (K: Type, V: Type) -> Type = { keys: Array(K), values: Array(V) } # Обязательно возвращает: Type
│
├── Функция пространства имён
│ └── draw: (p: Point, surface: Surface) -> Void = ...
│ Point.draw = draw[0] # Явная привязка для синтаксиса вызова .
│
└── Обобщённая функция
└── map: (T: Type, R: Type) -> ((list: List(T), f: (x: T) -> R) -> List(R)) # Не возвращает Type, HM выводит как функциюУровни метатипа (внутреннее представление компилятора)
Компилятор внутренне поддерживает вселенную уровней level: selfpointnum (хранится как строка, теоретически бесконечно расширяемая).
| Level | Описание |
|---|---|
Type0 | Повседневные типы (Int, Float, Point) |
Type1 | Конструкторы типов (List, Maybe) |
Type2+ | Конструкторы высших порядков |
Пользователь никогда не видит эти числа, только : Type.
Изоморфизм Curry-Howard: типы как суждения, программы как доказательства
Унифицированный синтаксис YaoXiang name: type = value выбран не случайно — он является прямым отображением изоморфизма Curry-Howard (Curry-Howard correspondence). Этот изоморфизм раскрывает глубокую истину: система типов и логическая система — две стороны одной медали.
| Логика (суждение) | Система типов (YaoXiang) | Пример |
|---|---|---|
| Суждение P | Тип T | Int, Bool |
| Доказательство P | Значение типа T | 42: Int, true: Bool |
| P → Q (импликация) | Функциональный тип (P) -> Q | (x: Int) -> Bool |
| P ∧ Q (конъюнкция) | Record type { p: P, q: Q } | { x: Int, y: Bool } |
| ∀x.P(x) (универсальная квантификация) | Обобщённая функция (T: Type) -> ... | map: (T: Type, R: Type) -> ... |
| P ⊕ Q (дизъюнкция) | Enum / tagged union | Maybe: (T: Type) -> Type = { ... } |
Значение name: type = value в рамках Curry-Howard:
// "x: Int = 42" читается как: "существует доказательство типа Int, называемое x, со значением 42"
x: Int = 42
// "add: (a: Int, b: Int) -> Int = a + b" читается как:
// "существует доказательство импликации: при наличии доказательств a и b типа Int можно построить доказательство типа Int"
add: (a: Int, b: Int) -> Int = a + b
// "Point: Type = { x: Float, y: Float }" читается как:
// "Point — это суждение, доказательство которого требует одновременного предоставления доказательств x и y типа Float"
Point: Type = { x: Float, y: Float }Почему это важно?
Логическая непротиворечивость = безопасность типов: если система типов позволяет сконструировать значение типа
Tбез какого-либо допустимого представления во время выполнения, это подобно допущению доказательства ложного суждения — система рушится. Curry-Howard говорит нам: язык с безопасной системой типов естественным образом является непротиворечивой логической системой.Уровни вселенной как необходимое условие: как подробно описано ниже, если позволить
Type: Type(«тип типа тоже является типом»), возникает парадокс Рассела (в теории типов проявляющийся как парадокс Жирара). ИерархияType₀ : Type₁ : Type₂ : ...в YaoXiang гарантирует, что каждый тип принадлежит определённому уровню, формируя никогда не замыкающуюся восходящую цепочку, что фундаментально предотвращает парадоксы. Это означает, что система типов YaoXiang является логически непротиворечивой в смысле Curry-Howard.Теоретическое обоснование унифицированного синтаксиса: то, что
name: type = valueможет охватить одной синтаксической формой все концепции — переменные, функции, типы, интерфейсы, обобщения — объясняется тем, что в рамках Curry-Howard всё это одно и то же — предоставление доказательства суждения. Переменные — свидетельства суждений, функции — свидетельства импликаций, record types — свидетельства конъюнкций, обобщения — свидетельства универсальной квантификации. Унифицированный синтаксис — не случайный дизайн, а естественное следствие изоморфизма Curry-Howard.
Дополнительное чтение: Wadler, P. (2015). "Propositions as Types." Communications of the ACM, 58(12), 75–84. В этой статье доступным языком объясняется история и значение изоморфизма Curry-Howard.
Синтаксическое определение
1. Объявление переменных
// Основной синтаксис
x: Int = 42
name: String = "Alice"
flag: Bool = true
// Вывод типов (можно опустить)
y = 100 // Выводится как Int2. Определение функций
// Форма с одиночным выражением (непосредственно возвращает значение, return не нужен)
add: (a: Int, b: Int) -> Int = a + b
greet: (name: String) -> String = "Hello, ${name}!"
// Форма с блоком кода (必须有 return для возврата значения)
process: (x: Int) -> Int = {
a = x * 2
b = a + 1
return b
}
// Многострочный блок кода
calc: (x: Float, y: Float, op: String) -> Float = {
return match op {
"+" -> x + y,
"-" -> x - y,
_ -> 0.0
}
}
// Void-функция (return не нужен в блоке кода)
print: (msg: String) -> Void = {
console.write(msg)
}Правила возврата
Возвращаемое значение зависит от формы правой части =:
| Запись | Возвращаемое значение |
|---|---|
= expr (без фигурных скобок) | Непосредственно возвращает expr |
= { ... } (с фигурными скобками) | Обязательно использовать return, иначе возвращает Void |
# Одиночное выражение: непосредственно возвращает значение, return не нужен
add: (a: Int, b: Int) -> Int = a + b
# Блок кода: обязательно return для возврата значения
process: (x: Int) -> Int = {
a = x * 2
b = a + 1
return b
}
# Void-функция: return не нужен
print: (msg: String) -> Void = {
console.write(msg)
}Обоснование дизайна:
{ ... }— это вычислительная единица с управлением зависимостями (см. ниже), семантика возврата которой отличается от одиночного выражения. Фигурные скобки вводят контекст с несколькими операторами, поэтому требуется явныйreturnдля устранения неоднозначности «является ли последнее выражение возвращаемым значением».
Семантика {}: вычислительная единица с управлением зависимостями
{ ... } в YaoXiang — это не просто блок кода — это вычислительная единица с управлением зависимостями. Эта семантика остаётся неизменной в теле функции, инициализации переменных и spawn:
Основные правила:
- Операторы присваивания внутри
{}автоматически упорядочиваются по зависимостям, а не по порядку записи - Операции с готовыми зависимостями выполняются немедленно, при отсутствии — блокируются в ожидании
- Явный возврат значения через
return(см. правила возврата)
# Управление зависимостями: b зависит от a, компилятор автоматически упорядочивает
result: Int = {
b = a + 1 # Зависит от a → автоматически ставится после a
a = 10 # Нет зависимостей → может выполняться первым
return b # Возвращает 11
}Отличие от одиночного выражения:
= expr(без фигурных скобок) — простое связывание с непосредственным возвратом значения;= { ... }(с фигурными скобками) вводит контекст вычисления с управлением зависимостями, допускающий многооператорный контекст и явныйreturn.
Блоки spawn
spawn { ... } — единственный примитив параллелизма в YaoXiang. Он использует семантику {} с управлением зависимостями для автоматического распараллеливания:
- Прямые дочерние присваивания внутри
spawn { ... }автоматически создают параллельные задачи - Задачи с готовыми зависимостями выполняются немедленно и параллельно
- Вызывающая сторона блокируется до завершения всех дочерних задач
result = spawn {
a = fetch_data("url1") # Задача 1
b = fetch_data("url2") # Задача 2 (нет зависимости от a, выполняется параллельно)
c = process(a, b) # Зависит от a, b → выполняется после завершения обоих
return c
}
// Вызывающая сторона блокируется здесь до завершения всех задач в блоке spawnПодробное определение: Полная семантика
spawn, правила создания задач и модель блокировки описаны в008-runtime-concurrency-model.md.
Блоки unsafe
unsafe { ... } используется для определения непрозрачных типов и операций с сырыми указателями. Он использует семантику возврата {} для передачи определения типа во внешнюю область видимости:
Основные правила:
- Внутри
unsafe {}можно определять типы и оперировать сырыми указателями - Использование
returnдля передачи определения типа во внешнюю область видимости - Возвращённые типы доступны за пределами
unsafe {} - Доступ к полям типа требует unsafe-разрешения
# Определение непрозрачного типа внутри unsafe-блока
SqliteDb = unsafe {
SqliteDb: Type = {
handle: *Void # Сырой указатель
}
return SqliteDb
}
# SqliteDb доступен за пределами unsafe-блока
db = sqlite3_open("test.db")
# ❌ Ошибка компиляции: поле handle требует unsafe-разрешения
handle = db.handle
# ✅ Через вызов метода
db.close()Подробное определение: Полная семантика
unsafe, определение FFI-типов и привязка методов описаны вffi.md.
3. Определение типов
Определение типа является ядром унифицированного синтаксиса YaoXiang, включая поля, значения по умолчанию, привязанные методы, реализации интерфейсов:
Базовые типы
Record type: список полей, типы полей могут быть произвольными тип-выражениями.
Point: Type = {
x: Float,
y: Float
}Поля со значениями по умолчанию: поля могут иметь значения по умолчанию, необязательны при конструировании.
Point: Type = {
x: Float = 0,
y: Float = 0
}Использование:
Point() → Point(x=0, y=0)
Point(x=1) → Point(x=1, y=0)
Point(x=1, y=2) → Point(x=1, y=2)Поля без значений по умолчанию: обязательны при конструировании.
Point2: Type = {
x: Float,
y: Float
}Использование:
Point2(x=1, y=2) //✓
Point2() //✗
Point2(x=1) //✗Встроенные типы
Идентификаторная система YaoXiang делится на три уровня, распознаваемых последовательно разными этапами компилятора:
- Ключевые слова (независимый токен парсера) — ключевые слова управления потоком и объявлений, такие как
if,match,pub,return - Зарезервированные литералы (независимый токен парсера) —
true,false,void,Type, не могут использоваться как обычные идентификаторы - Встроенные имена типов (предварительно зарегистрированы в type checker) — парсер рассматривает их как обычные идентификаторы, type checker отвечает за их解析. Не являются зарезервированными словами, могут быть shadowed (не рекомендуется)
Различие между void (нижний регистр, зарезервированный литерал) и Void (верхний регистр, встроенное имя типа): void — это значение-литерал (равно единственному значению Unit), Void — это имя типа (равно типу Unit, логическое ⊤). let x: Void = void — корректно.
Предопределённые встроенные имена типов:
| Тип | Логическое соответствие | Описание |
|---|---|---|
Never | ⊥ (ложь/пустой тип) | Ноль конструкторов, ни одно значение не может обитать в этом типе. Обозначает «невозможно» — расхождение, panic, мёртвый код. Never <: T верно для любого T (принцип взрыва). Функция, возвращающая Never, никогда не возвращается нормально. Не является ключевым словом, это встроенное имя типа. |
Void | ⊤ (истина/Unit) | Ровно один обитатель (значение void по умолчанию). x: Void = <по умолчанию> корректно. Соответствует единичному элементу суммы типов и единичному элементу произведения типов — Void это произведение с нулём полей (Unit), Never это сумма с нулём вариантов. |
Int | — | Знаковое целое |
Float | — | Число с плавающей точкой |
Bool | — | Булево значение: true / false |
Char | — | Символ Unicode |
String | — | Строка |
Привязка методов
Способ 1: непосредственная привязка внешней функции внутри определения типа
distance: (a: Point, b: Point) -> Float = { ... }
Point: Type = {
x: Float = 0,
y: Float = 0,
distance = distance[0] # Привязка к позиции 0, после каррирования method: (b: Point) -> Float
}
// Вызов: p1.distance(p2) → distance(p1, p2)Способ 2: анонимная функция + позиционная привязка
Point: Type = {
x: Float = 0,
y: Float = 0,
distance: ((a: Point, b: Point) -> Float)[0] = ((a, b) => {
dx = a.x - b.x
dy = a.y - b.y
return (dx * dx + dy * dy).sqrt()
})
}
// Синтаксис: ((params) => body)[position]
// Вызов: p1.distance(p2) → distance(p1, p2)Реализация интерфейсов
Имя интерфейса записывается в теле типа, компилятор автоматически проверяет его реализацию
Drawable: Type = {
draw: (Surface) -> Void,
bounding_box: () -> Rect
}
Serializable: Type = {
serialize: () -> String
}
Point: Type = {
x: Float,
y: Float,
Drawable, # Реализует интерфейс Drawable
Serializable # Реализует интерфейс Serializable
}Определение интерфейсов
Интерфейс = record type, все поля которого являются функциями
Drawable: Type = {
draw: (Surface) -> Void,
bounding_box: () -> Rect
}
Serializable: Type = {
serialize: () -> String
}
// Пустой тип/пустой интерфейс
EmptyType: Type = {}
Empty: Type = {}Определение функций пространства имён
Префикс Type.name означает принадлежность к пространству имён, и только. Он не вызывает никакой неявной привязки.
# Функция пространства имён: обычная функция в пространстве имён Point
Point.draw: (p: &Point, surface: Surface) -> Void = {
surface.plot(p.x, p.y)
}
Point.serialize: (p: &Point) -> String = {
return "Point(${p.x}, ${p.y})"
}
// Вызов: это обычный вызов функции
Point.draw(p, screen)
Point.serialize(p)Примечание:
selfне является ключевым словом, это просто договорённость об имени параметра. Записьp,this,xдаёт полностью идентичный результат. Компилятор смотрит на тип, а не на имя параметра.
Привязка методов (единственный способ)
Чтобы синтаксис вызова метода p.draw(screen) через . заработал, необходима явная привязка. Синтаксис [position] — единственный механизм привязки функции в качестве «метода» (подробный синтаксис см. RFC-004).
// Определение функции
draw: (p: &Point, surface: Surface) -> Void = {
surface.plot(p.x, p.y)
}
// Явная привязка — после этого появляется синтаксис p.draw(screen)
Point.draw = draw[0] # Параметр на позиции 0 (&Point) заполняется вызывающей стороной
// Использование
p.draw(screen) // Синтаксический сахар → draw(&p, screen)
Point.draw(p, screen) // Оба способа вызова эквивалентны
// Без [0] = без привязки. Point.draw — обычный псевдоним функции, без синтаксиса .
Point.draw = draw // Без привязки: доступен только Point.draw(p, screen)Поведение по умолчанию: без [n] = без привязки какого-либо параметра. Пользователь должен явно решить, какие параметры заполняются вызывающей стороной.
Мультипозиционная привязка:
// Привязка нескольких позиций (автоматическое каррирование)
Point.transform = transform_points[0, 1]
// Вызов: p1.transform(p2)(2.0) → transform_points(p1, p2, 2.0)Обратная операция (метод → обычная функция):
// Извлечение функции из привязки
draw_point: (p: &Point, surface: Surface) -> Void = Point.draw4. Композиция интерфейсов
// Композиция интерфейсов = пересечение типов
DrawableSerializable: Type = Drawable & Serializable
// Использование типа пересечения
process: (T: Drawable & Serializable) -> ((item: T, screen: Surface) -> String) = {
item.draw(screen)
return item.serialize()
}5. Обобщённые типы
// Базовые обобщения (RFC-011 Phase 1)
List: (T: Type) -> Type = {
data: Array(T),
length: Int,
push: (T:Type)-((self: List(T), item: T) -> Void),
get: (T:Type)->((self: List(T), index: Int) -> Maybe(T))
}
// Конкретная инстанциация (RFC-023 синтаксис)
IntList: Type = List(Int)
IntList.push = {
self.data.append(item)
self.length = self.length + 1
}
List.push = (type: Type) -> {
return (self: List(type), item: type) -> {
self.data.append(item)
self.length = self.length + 1
}
}
IntList.push(Int)(self, item) // Пример вызова
// Обобщённые методы (RFC-023 синтаксис: параметры типа автоматически выводятся в точке вызова)
List.push: (self: List(T), item: T) -> Void = {
self.data.append(item)
self.length = self.length + 1
}
List.get: (self: List(T), index: Int) -> Maybe(T) = {
if index >= 0 && index < self.length {
return Maybe.Just(self.data[index])
} else {
return Maybe.Nothing
}
}6. Синтаксис вызова обобщений
Вызов обобщённых типов и обобщённых функций унифицированно использует синтаксис (). [] не используется ни в каком контексте обобщений.
Основные правила:
()выполняет всё: применение типа, вызов функции, конструирование значения — всё через()
# Аннотация типа
numbers: List(Int) = List(1, 2, 3)
# Пустой контейнер: T приходит слева
empty: List(Int) = List()
# Вызов обобщённой функции — тип автоматически «перетекает» из параметров
strings = map(numbers, f)
// T=Int приходит из numbers: List(Int)
// R=String приходит из f: (Int) -> StringType слева, значение справа:
name: type = value— параметры Type объявляются слева, справа всегда конкретное значение. Для пустого контейнераList()параметрTдолжен приходить из аннотации типа слева.Информация о типе пишется один раз — в объявлении параметра, компилятор несёт его дальше:
numbers: List(Int) = List(1, 2, 3) // Int пишется один раз слева
f: (Int) -> String = (x) => x.to_string()
strings = map(numbers, f) // T=Int, R=String автоматически приходят из типов numbers и f- Конструирование значения выводит тип из элементов:
x = List(1, 2, 3) # Выводится как List(Int)
y = List("a", "b") # Выводится как List(String)
z = List() # ❌ Ошибка компиляции: T невозможно вывести
z: List(Int) = List() # ✅ T=Int приходит из аннотации слева- Псевдонимы типов:
IntList: Type = List(Int)
StringToInt: Type = (String) -> Int
Matrix3x3: Type = Matrix(Float, 3, 3)Сравнение со старым синтаксисом:
List[Int]→List(Int),List[Int]()→List(),List[Int](1,2,3)→List(1,2,3). Старый синтаксис обобщений[]полностью удалён.[]используется только для литералов массивов/списков и индексного доступа.
Примеры
Полный пример
// ======== 1. Определение интерфейсов ========
// Интерфейс = record type, все поля которого являются функциональными типами
// В интерфейсе параметр self не нужен — интерфейс определяет только «сигнатуру функции после удаления позиции вызывающего»
Drawable: Type = {
draw: (surface: Surface) -> Void,
bounding_box: () -> Rect
}
Serializable: Type = {
serialize: () -> String
}
Transformable: Type = {
translate: (dx: Float, dy: Float) -> Transformable, // Возвращает тип интерфейса, конкретная реализация возвращает свой тип
scale: (factor: Float) -> Transformable
}
// ======== 2. Определение типов ========
Point: Type = {
x: Float,
y: Float,
Drawable,
Serializable,
Transformable
}
Rect: Type = {
x: Float,
y: Float,
width: Float,
height: Float,
Drawable,
Serializable,
Transformable
}
// ======== 3. Реализация методов (обычные функции + явная привязка)========
// Определение функции (self — просто договорённость, не ключевое слово)
draw: (p: &Point, surface: Surface) -> Void = {
surface.plot(p.x, p.y)
}
bounding_box: (p: &Point) -> Rect = {
return Rect(p.x - 1, p.y - 1, 2, 2)
}
serialize: (p: &Point) -> String = {
return "Point(${p.x}, ${p.y})"
}
translate: (p: &Point, dx: Float, dy: Float) -> Point = {
return Point(p.x + dx, p.y + dy)
}
scale: (p: &Point, factor: Float) -> Point = {
return Point(p.x * factor, p.y * factor)
}
distance: (p1: &Point, p2: &Point) -> Float = {
dx = p1.x - p2.x
dy = p1.y - p2.y
return (dx * dx + dy * dy).sqrt()
}
// Явная привязка — после привязки появляется синтаксис вызова .
Point.draw = draw[0]
Point.bounding_box = bounding_box[0]
Point.serialize = serialize[0]
Point.translate = translate[0]
Point.scale = scale[0]
Point.distance = distance[0]
// Методы Rect аналогично
draw: (r: &Rect, surface: Surface) -> Void = {
surface.draw_rect(r.x, r.y, r.width, r.height)
}
Rect.draw = draw[0]
bounding_box: (r: &Rect) -> Rect = r
Rect.bounding_box = bounding_box[0]
serialize: (r: &Rect) -> String = {
return "Rect(${r.x}, ${r.y}, ${r.width}, ${r.height})"
}
Rect.serialize = serialize[0]
translate: (r: &Rect, dx: Float, dy: Float) -> Rect = {
return Rect(r.x + dx, r.y + dy, r.width, r.height)
}
Rect.translate = translate[0]
scale: (r: &Rect, factor: Float) -> Rect = {
return Rect(r.x * factor, r.y * factor, r.width * factor, r.height * factor)
}
Rect.scale = scale[0]
// ======== 4. Использование ========
// Создание экземпляров
p: Point = Point(1.0, 2.0)
r: Rect = Rect(0.0, 0.0, 10.0, 20.0)
// Вызов методов (синтаксический сахар)
p.draw(screen)
r.draw(screen)
// Обычный вызов метода (прямой вызов)
d: Float = distance(p, Point(0.0, 0.0))
// Цепочечный вызов
p2: Point = p.translate(1.0, 1.0).scale(2.0)
// Присваивание интерфейсу
drawables: List(Drawable) = [p, r]
for d in drawables {
d.draw(screen)
}
// Обобщённая функция (RFC-023 синтаксис: параметры типа опущены при вызове, выводятся автоматически)
process_all: (items: List(T)) -> Void = {
for item in items {
print(item.serialize())
}
}
process_all([p, r])Детальное проектирование
Алгоритм проверки интерфейсов
fn check_type_implements_interface(
typ: &Type,
iface: &Type
) -> Result<(), TypeError> {
// Для каждого поля интерфейса (функциональное поле)
for (field_name, iface_field) in &iface.fields {
// Проверяем, есть ли у типа метод с таким же именем
if let Some(method) = typ.methods.get(field_name) {
// Проверяем совместимость сигнатуры метода
// Поле интерфейса: (Surface) -> Void
// Сигнатура метода: (Point, Surface) -> Void
// Сравнение: после удаления параметра self должно совпадать
if !method_signature_matches(method, iface_field.type_) {
return Err(TypeError::MethodSignatureMismatch {
type_name: typ.name,
interface_name: iface.name,
method_name: field_name,
});
}
} else {
return Err(TypeError::MissingMethod {
type_name: typ.name,
interface_name: iface.name,
method_name: field_name,
});
}
}
Ok(())
}Прямое присваивание интерфейсу и оптимизация времени компиляции
Интерфейсные типы поддерживают прямое присваивание, компилятор автоматически выбирает оптимальную стратегию вызова на основе типа правой части присваивания:
// Прямое присваивание конкретного типа → конкретный тип определим во время компиляции, вызов с нулевыми накладными расходами
d: Drawable = Circle(1)
d.draw(screen) // После компиляции: прямой вызов circle_draw(screen), без vtable
// Возврат из функции → конкретный тип не определим во время компиляции, используется vtable
d: Drawable = get_shape()
d.draw(screen) // Поиск метода через vtable
// Гетерогенная коллекция → используется vtable
shapes: List(Drawable) = [Circle(1), Rect(2, 3)]
for s in shapes {
s.draw(screen) // Поиск метода через vtable
}Стратегия оптимизации времени компиляции:
| Сценарий | Результат вывода | Способ вызова |
|---|---|---|
d: Drawable = Circle(1) | Конкретный тип Circle | Прямой вызов (нуль накладных) |
d: Drawable = get_shape() | Неизвестен | vtable |
shapes: List(Drawable) = [...] | Гетерогенный | vtable |
Правила:
- Когда правая часть является конкретным конструктором типа и определима во время компиляции, генерируется прямой вызов IR
- Когда тип правой части невозможно определить во время компиляции, происходит откат к механизму vtable
- vtable гарантирует корректность полиморфизма во время выполнения
Поддержка утиной типизации
// Пока есть одинаковые методы, можно присвоить типу интерфейса
CustomPoint: Type = {
draw: (self: CustomPoint, surface: Surface) -> Void,
x: Float,
y: Float
}
custom: CustomPoint = CustomPoint(
(self: CustomPoint, surface: Surface) => surface.plot(self.x, self.y),
1.0,
2.0
)Синтаксические изменения
| Раньше | Теперь |
|---|---|
type Point = Point(x: Float, y: Float) | type Point = { x: Float, y: Float } |
type Result(T, E) = ok(T) | err(E) | Result: (T: Type, E: Type) -> Type = { ok: (T) -> Result(T, E), err: (E) -> Result(T, E) } |
Требовалось ключевое слово impl | Ключевое слово не нужно, имя интерфейса записывается после тела типа |
Устарело: синтаксис варианта |
Объявление об устаревании (2026-07-25, issue #203): Синтаксис варианта
|официально устарел и удалён из реализации.
Следующие записи больше не поддерживаются:
type Color = red | green | blue # ❌ Устарело
type Result(T, E) = ok(T) | err(E) # ❌ Устарело
type Option(T) = some(T) | none # ❌ УстарелоВместо этого повсеместно используется record type для выражения типов суммы (sum type). Когда все поля record type являются функциями, и каждая функция возвращает этот же тип, он становится типом суммы:
Color: Type = {
red: () -> Color,
green: () -> Color,
blue: () -> Color
}
Result: (T: Type, E: Type) -> Type = {
ok: (T) -> Result(T, E),
err: (E) -> Result(T, E)
}
Option: (T: Type) -> Type = {
some: (T) -> Option(T),
none: () -> Option(T)
}Обоснование дизайна:
- Устранение особых случаев:
|— единственная форма синтаксиса в BNF, отличная отname: type = value. После удаленияtype_exprполностью унифицирован, парсер больше не нуждается в поддержке отдельного пути и lookahead-backtracking для variant-типов. - Математическая эквивалентность: В изоморфизме Curry-Howard дизъюнкция P ⊕ Q соответствует типу суммы, который эквивалентен record type «все поля которого являются функциями, возвращающими сам тип». Оба выражают одну семантику, две системы синтаксиса не нужны.
- Нулевая разрушительность: До удаления
|синтаксис полуподдерживался в парсере (варианты без параметров解析ровались, но типы параметров терялись при мономорфизации), никакой пользовательский код не зависел от него. - Упрощение AST: Узел
Type::Variant(Vec<VariantDef>)удалён, все variant-типы统一走Type::Structпути, все специальные ветки в downstream typecheck/mono/formatter полностью устранены.
Примечание: Семантические атрибуты типов суммы (например, исчерпывающая проверка match, размещение tagged union в памяти) выводятся typecheck-слоем из структуры
Type::Struct, а не из отдельного AST-узла.
Синтаксический дизайн: именованные функции по своей сути являются синтаксическим сахаром Lambda
Ключевое понимание
Именованные функции и Lambda-выражения — это одно и то же! Единственное различие: именованная функция просто даёт Lambda имя.
// Эти две формы по сути полностью идентичны
add: (a: Int, b: Int) -> Int = a + b // Именованная функция (рекомендуется)
add: (a: Int, b: Int) -> Int = (a, b) => a + b // Lambda-форма (полностью эквивалентна)Модель синтаксического сахара
// Именованная функция = Lambda + имя
name: (Params) -> ReturnType = body
// По сути это
name: (Params) -> ReturnType = (params) => bodyКлючевой момент: Когда сигнатура полностью объявляет типы параметров, имена параметров в голове Lambda становятся избыточными и могут быть опущены.
Правила области видимости параметров
Параметры перекрывают внешние переменные: область видимости параметров в сигнатуре перекрывает тело функции, приоритет у внутренней области выше.
x = 10 // Внешняя переменная
double: (x: Int) -> Int = x * 2 // ✅ Параметр x перекрывает внешний x, результат 20Гибкость позиции аннотации
Аннотация типа может находиться в любой из следующих позиций, достаточно аннотировать хотя бы в одном месте:
| Позиция аннотации | Форма | Примечание |
|---|---|---|
| Только в сигнатуре | double: (x: Int) -> Int = x * 2 | ✅ Рекомендуется |
| Только в голове Lambda | double = (x: Int) => x * 2 | ✅ Допустимо |
| В обоих местах | double: (x: Int) -> Int = (x) => x * 2 | ✅ Избыточно, но допустимо |
Полный пример
// ✅ Рекомендуется: сигнатура полная, голова Lambda опущена
add: (a: Int, b: Int) -> Int = a + b
inc: (x: Int) -> Int = x + 1
main: () -> Void = { print("hi") }
// ✅ Допустимо: тип в голове Lambda
double = (x: Int) => x * 2
// ✅ Допустимо: обе стороны аннотированы
double: (x: Int) -> Int = (x) => x * 2Преимущества дизайна
| Характеристика | Преимущество |
|---|---|
| Лаконичность | Когда сигнатура полная, не нужно повторно писать имена параметров |
| Гибкость | Сохранена Lambda-форма, используй какую предпочитаешь |
| Единство | Единообразие с объявлением переменной x: Int = 42 |
| Наглядность | name: Type = body напрямую соответствует «существует name, тип Type, значение body» |
Компромиссы
Преимущества
| Преимущество | Описание |
|---|---|
| Предельная унификация | Одно синтаксическое правило охватывает все случаи |
| Теоретическая элегантность | Идеально симметричное name: type = value |
| Без новых ключевых слов | Повторное использование существующих синтаксических элементов |
| Лёгкость реализации | Компилятору нужно обрабатывать только одну форму объявления |
| Лёгкость изучения | Запомнив одну модель, можно писать весь код |
| Лёгкость расширения | Новые возможности естественно вписываются в эту модель |
Недостатки
| Недостаток | Описание |
|---|---|
| Соглашения об именовании | Методы должны следовать именованию Type.method |
| Многословность | Полный синтаксис длиннее упрощённого, но выводится |
| Кривая обучения | Требуется понимание унифицированной модели |
Меры смягчения
// 1. Понятные сообщения об ошибках
// Пример ошибки компиляции:
// Error: Point does not implement Serializable
// Required method 'serialize: (self: Point) -> String' not found
// Note: Define Point.serialize to implement Serializable
// 2. Вывод типов
// Типы можно опустить, компилятор выведет
Point.draw = (self: Point, surface: Surface) => surface.plot(self.x, self.y)
// 3. Подсказки IDE
// IDE автоматически подсказывает отсутствующие методыРиски
| Риск | Влияние | Меры смягчения |
|---|---|---|
| Сложность парсинга | Унифицированный синтаксис может увеличить сложность парсинга | Использование рекурсивного нисходящего парсера |
| Накладные расходы производительности | Поиск в vtable может иметь дополнительные накладные расходы | Оптимизация мономорфизации на этапе компиляции |
Пасхалка 🎮: Источник языка
✨ Type: Type = Type ✨
// Попытка определить тип типа...
Type: Type = TypeПредупреждение: Это невыразимое!
╔══════════════════════════════════════════════════════════════╗
║ ║
║ 一生二,二生三,三生万物。 ║
║ 易有太极,是生两仪。 ║
║ ║
║ Type: Type = Type ║
║ 此乃爻象之源,语言之边界。 ║
║ 编译器在此沉默,哲学在此驻足。 ║
║ ║
║ 感谢你触达语言的哲学边界。 ║
║ ║
╚══════════════════════════════════════════════════════════════╝Примечание: Компилятор не может корректно обработать
Type: Type = Type(это приведёт к парадоксу уровней вселенной Type0/Type1), но мы намеренно сохраняем эту «пасхалку» — при попытке компиляции вы получите дзенское сообщение от создателя языка. Это не только техническая граница, но и дань уважения философии типов YaoXiang.
Приложения
BNF-грамматика
program ::= statement*
statement ::= declaration | expression
# Унифицированное объявление: name: Type = expression
declaration ::= identifier ':' type_expr '=' expression
# Тип-выражение
type_expr ::= identifier
| identifier '(' type_expr (',' type_expr)* ')' # Применение типа
| '(' type_expr (',' type_expr)* ')' '->' type_expr # Функциональный тип
| '{' type_field* '}' # Record/интерфейсный тип
| 'Type' # Метатип
type_field ::= identifier ':' type_expr
| identifier # Ограничение интерфейса
# Параметры обобщений: как часть функционального типа, например (T: Type, R: Type) -> (...)
# Не требуется отдельное BNF-правило — параметр : Type — обычный параметр функции
# Выражение
expression ::= literal
| identifier
| identifier '(' expression (',' expression)* ')' # Вызов функции / вызов конструктора
| '(' expression (',' expression)* ')' # Кортеж
| expression '.' identifier '(' arguments? ')' # Вызов метода
| lambda
| '{' field ':' expression (',' field ':' expression)* '}'
arguments ::= expression (',' expression)*
lambda ::= '(' parameter_list? ')' '=>' block
block ::= expression | '{' expression* '}'Глоссарий
| Термин | Определение |
|---|---|
| Объявление | Оператор присваивания формы name: type = value |
| Record type | Тип { ... } с именованными полями |
| Интерфейс | Record type, все поля которого являются функциональными типами |
| Обобщённый тип | Тип, определённый как Name: (T: Type) -> Type = { ... }, принимающий параметр-тип |
| Функция пространства имён | Функция формы Type.name, принадлежащая пространству имён Type. Не подразумевает никакой привязки |
| Привязка метода | Type.name = func[n], привязка позиции n функции func как вызывающей, делающая синтаксис obj.name(args) доступным |
| Обобщённая функция | Функция, использующая синтаксис (T: Type), параметр-тип передаётся как первый параметр |
| Метатип | Type, единственный маркер уровня типов в языке |
Жизненный цикл и судьба
┌─────────────┐
│ Черновик │ ← Текущее состояние
└──────┬──────┘
│
▼
┌─────────────┐
│ На ревью │ ← Открыто обсуждение сообщества и обратная связь
└──────┬──────┘
│
├──────────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Принято │ │ Отклонено │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ accepted/ │ │ rfc/ │
│(официальный │ │(оставлен на │
│ дизайн) │ │ месте) │
└─────────────┘ └─────────────┘