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 = {
"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: () -> Void = { 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.
Изоморфизм Карри–Ховарда: тип как утверждение, программа как доказательство
Единый синтаксис YaoXiang name: type = value выбран не произвольно — он является прямым отражением изоморфизма Карри–Ховарда (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 в рамках Карри–Ховарда:
// "x: Int = 42" читается как: "существует доказательство типа Int по имени x со значением 42"
x: Int = 42
// "add: (a: Int, b: Int) -> Int = a + b" читается как:
// "существует доказательство импликации: даны доказательства Int a и b, можно построить доказательство Int"
add: (a: Int, b: Int) -> Int = a + b
// "Point: Type = { x: Float, y: Float }" читается как:
// "Point — это утверждение, доказательство которого требует одновременно доказательство Float x и доказательство Float y"
Point: Type = { x: Float, y: Float }Почему это важно?
Логическая непротиворечивость = типобезопасность: если система типов разрешает построить значение типа
Tбез какого-либо легитимного представления в рантайме, это подобно тому, как логика разрешает доказать ложное утверждение — система рушится. Карри–Ховард говорит нам: типобезопасный язык естественным образом является непротиворечивой логической системой.Иерархия вселенных — необходимое условие: как подробно описано ниже, разрешение
Type: Type(то есть «тип типа тоже тип») приводит к парадоксу Рассела (в теории типов — парадоксу Жирара). СлоиType₀ : Type₁ : Type₂ : ...в YaoXiang гарантируют, что каждый тип принадлежит строго своему уровню, образуя бесконечно восходящую цепочку, что фундаментально устраняет парадоксы. Это означает, что система типов YaoXiang в смысле Карри–Ховарда логически непротиворечива.Теоретическое обоснование единого синтаксиса:
name: type = valueспособен одной формой синтаксиса покрыть переменные, функции, типы, интерфейсы и дженерики именно потому, что в рамках Карри–Ховарда это всё одна сущность — предоставление доказательств для утверждений. Переменная — свидетельство утверждения, функция — свидетельство импликации, record — свидетельство конъюнкции, дженерики — свидетельство квантора всеобщности. Единый синтаксис — не случайность дизайна, а естественное следствие изоморфизма Карри–Ховарда.
Дополнительное чтение: Wadler, P. (2015). "Propositions as Types." Communications of the ACM, 58(12), 75–84. Эта статья доступно объясняет историю и значение изоморфизма Карри–Ховарда.
Определение синтаксиса
1. Объявление переменной
// Базовый синтаксис
x: Int = 42
name: String = "Alice"
flag: Bool = true
// Вывод типа (можно опустить)
y = 100 // выводится как Int2. Определение функции
Значение блока = хвостовое выражение, return осуществляет выход из функции (тип Never) — подробности см. в RFC-010a.
// Форма с одним выражением
add: (a: Int, b: Int) -> Int = a + b
greet: (name: String) -> String = "Hello, ${name}!"
// Форма блока кода: значение — хвостовое выражение
process: (x: Int) -> Int = {
a = x * 2
b = a + 1
b // хвостовое выражение → значение блока
}
// Многострочный блок кода
calc: (x: Float, y: Float, op: String) -> Float = {
match op {
"+" -> x + y,
"-" -> x - y,
_ -> 0.0
} // match как хвостовое выражение
}
// Функция Void: хвостовое выражение имеет тип Void
print: (msg: String) -> Void = {
console.write(msg) // console.write : Void
}Правила возврата
Значение блока = хвостовое выражение (единственный выход):
| Запись | Значение |
|---|---|
= expr (без фигурных скобок) | expr |
= { ...; e } (с фигурными скобками) | хвостовое выражение e |
= { ...; s } (последним идёт оператор/присваивание) | Void (значение присваивания — Void) |
= {} (пустой блок) | Void |
Семантика return: нелокальный выход, выход из ближайшей границы функции (а не «возврат в блок»), тип — Never. Never <: T для любого типа (принцип взрыва), поэтому return может встречаться в любой позиции возврата.
# Одно выражение: прямое возвращение значения
add: (a: Int, b: Int) -> Int = a + b
# Блок кода: значение — хвостовое выражение
process: (x: Int) -> Int = {
a = x * 2
b = a + 1
b
}
# Досрочный возврат: return проходит сквозь блок и выходит из функции (тип Never)
factorial: (n: Int) -> Int = {
if n <= 1 { return 1 }
n * factorial(n - 1) # хвостовое выражение
}Обоснование дизайна: { ... } — это вычислительная единица, управляемая зависимостями (см. ниже), и её семантика вычисления отличается от одночного выражения — фигурные скобки вводят многооператорный контекст, значение определяется хвостовым выражением, и неоднозначность вроде «является ли последнее выражение возвращаемым значением» отсутствует.
Семантика {}: вычислительная единица, управляемая зависимостями
{ ... } в YaoXiang — это не просто блок кода, а вычислительная единица, управляемая зависимостями. Эта семантика остаётся единообразной в телах функций, инициализаторах переменных и в spawn:
Основные правила:
- операторы присваивания внутри
{}автоматически упорядочиваются по зависимостям, а не по порядку записи; - готовые зависимости выполняются немедленно, при отсутствии — блокируются в ожидании;
- значение блока = хвостовое выражение (см. правила возврата);
return— нелокальный выход типаNever, осуществляющий выход из функции.
# Управление зависимостями: b зависит от a, компилятор автоматически упорядочивает
result: Int = {
b = a + 1 # зависит от a → автоматически после a
a = 10 # без зависимостей → может выполниться первым
b # хвостовое выражение → значение блока равно 11
}Отличие от одночного выражения:
= expr(без фигурных скобок) — простая привязка, возвращающая значение напрямую;= { ... }(с фигурными скобками) вводит контекст вычислений, управляемый зависимостями, допускает несколько операторов, и его значение задаётся хвостовым выражением.
Блок spawn
spawn { ... } — единственная примитива параллелизма в YaoXiang. Она использует семантику {}, управляемую зависимостями, для автоматического распараллеливания:
- непосредственные дочерние присваивания внутри
spawn { ... }автоматически создают параллельные задачи; - задачи, для которых зависимости удовлетворены, выполняются конкурентно немедленно;
- вызывающая сторона блокируется до завершения всех дочерних задач.
result = spawn {
a = fetch_data("url1") # задача 1
b = fetch_data("url2") # задача 2 (нет зависимости от a, выполняется параллельно)
c = process(a, b) # зависит от a, b → ожидает их завершения
c # хвостовое выражение → значение spawn
}
// вызывающая сторона блокируется здесь до завершения всех задач внутри spawnПодробное определение: полная семантика
spawn, правила создания задач и модель блокировки см. в008-runtime-concurrency-model.md.
Блок unsafe
unsafe { ... } используется для определения непрозрачных типов и работы с сырыми указателями. Он использует семантику вычисления {} для передачи определения типа во внешнюю область видимости (выход значения — хвостовое выражение):
Основные правила:
- внутри
unsafe {}можно определять типы и работать с сырыми указателями; - хвостовое выражение даёт значение
unsafe {}(определение типа передаётся во внешнюю область видимости); - возвращённый тип доступен за пределами
unsafe {}; - для доступа к полям типа требуются права unsafe.
# Определение непрозрачного типа внутри блока unsafe
SqliteDb = unsafe {
SqliteDb: Type = {
handle: *Void # сырой указатель
}
SqliteDb # хвостовое выражение → значение блока unsafe
}
# 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) — парсер рассматривает их как обычные идентификаторы, разбором занимается проверщик типов. Не являются зарезервированными словами, могут быть затенены (не рекомендуется).
Различие между void (строчное, литеральное зарезервированное слово) и Void (прописное, имя встроенного типа): void — литерал значения (равен единственному значению Unit), Void — имя типа (равен типу Unit, логически ⊤). let x: Void = void допустимо.
Предопределённые имена встроенных типов:
| Тип | Логический аналог | Описание |
|---|---|---|
Never | ⊥ (ложь/пустой тип) | Нулевой набор конструкторов, никакое значение не может населять этот тип. Означает «невозможно» — расходимость, паника, мёртвый код. 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
(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 = {
"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)
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) -> {
(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 and index < self.length {
Maybe.Just(self.data[index])
} else {
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 = {
Rect(p.x - 1, p.y - 1, 2, 2)
}
serialize: (p: &Point) -> String = {
"Point(${p.x}, ${p.y})"
}
translate: (p: &Point, dx: Float, dy: Float) -> Point = {
Point(p.x + dx, p.y + dy)
}
scale: (p: &Point, factor: Float) -> Point = {
Point(p.x * factor, p.y * factor)
}
distance: (p1: &Point, p2: &Point) -> Float = {
dx = p1.x - p2.x
dy = p1.y - p2.y
(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 = {
"Rect(${r.x}, ${r.y}, ${r.width}, ${r.height})"
}
Rect.serialize = serialize[0]
translate: (r: &Rect, dx: Float, dy: Float) -> Rect = {
Rect(r.x + dx, r.y + dy, r.width, r.height)
}
Rect.translate = translate[0]
scale: (r: &Rect, factor: Float) -> Rect = {
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 | ключевое слово не требуется, имена интерфейсов записываются в конце тела типа |
Устаревший синтаксис: варианты с |
Объявление об устаревании (25.07.2026): синтаксис вариантов с
|официально устарел и удалён из реализации.
Следующие записи больше не поддерживаются:
type Color = red | green | blue # ❌ устарело
type Result(T, E) = ok(T) | err(E) # ❌ устарело
type Option(T) = some(T) | none # ❌ устарелоСуммы и типы выражаются единообразно через record 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полностью унифицирована, парсеру больше не нужно поддерживать отдельный путь и просмотр с возвратом для вариантных типов. - Математическая эквивалентность: в рамках изоморфизма Карри–Ховарда дизъюнкция P ⊕ Q, соответствующая типу-сумме, эквивалентна record type, «поля которого — функции, возвращающие сам этот тип». Семантика та же, две формы синтаксиса не нужны.
- Нулевое разрушение: до удаления синтаксис
|в парсере поддерживался наполовину (варианты без аргументов разбирались, но типы аргументов терялись при мономорфизации), и ни один пользовательский код на него не полагался. - Упрощение AST: узел
Type::Variant(Vec<VariantDef>)удаляется, все вариантные типы проходят через путьType::Struct, специальные ветки в typecheck/mono/formatter полностью устранены.
Примечание: семантические свойства типа-суммы (конструирование варианта, проверка полноты match, размещение tagged union в памяти) выводятся на уровне typecheck из структуры
Type::Structи не зависят от отдельного узла AST. Полная семантика конструирования вариантов приведена в следующем разделе «Конструирование вариантов record-типа суммы (авторитетное определение)».
Конструирование вариантов record-типа суммы (авторитетное определение)
Данный раздел раскрывает критерии распознавания из раздела «Устаревший синтаксис: варианты с
|» в полную семантику (утверждено 25.09.2026, issue #341, предварительное условие для RFC-011b, фаза 2). Четыре решения: вызов с квалификацией типа, форма вызова вариантов с нулевой нагрузкой, самодостаточный вывод типа, полное или полностью отсутствующее повышение имени варианта.
Правило распознавания: всё или ничего
Record type считается типом-суммой при одновременном выполнении двух условий:
- Все поля тела типа имеют функциональный тип;
- Возвращаемый тип каждого поля после подстановки параметров типа (
Self/аргументы дженериков) равен самому этому record type.
Распознавание работает по принципу всё или ничего: при выполнении условий все функциональные поля одновременно повышаются до конструкторов вариантов; при невыполнении весь тип — обычный record (функциональные поля являются полями данных, хранящими значения функций), и смешанной формы «частично варианты, частично данные» не существует. Если требуется одновременно нести данные и вариантную семантику, моделируются два типа — record данных и тип-сумма, без слияния объявлений.
Форма вызова: с квалификацией типа, без голого имени
Единственная форма вызова конструктора вариантов — вызов с квалификацией типа: получение члена варианта на значении типа с последующим вызовом:
r1 = Result(Int, String).ok(5) // Result(Int, String), полезная нагрузка 5
c = Color.red() // вариант с нулевой нагрузкой: вызов в форме функции, согласующейся с сигнатурой поляФорма с голым именем не предоставляется (ok(5) не является вызовом конструктора). Причина: голое имя требует вывода принадлежности к типу-сумме и его аргументам типа из контекстного ожидаемого типа, тогда как в данном языке тип должен быть явно записываемым, вывод течёт однонаправленно из позиции ожидания (та же дисциплина, что и в RFC-011a, «Self — явный параметр типа, без магии»). В форме с квалификацией тип полностью самодостаточен и не требует контекста. Если в будущем голое имя и будет введено, оно может быть только «развёрткой-сахаром при однозначной определимости из контекста», а семантическим базисом останется определение с квалификацией из данного раздела.
Вывод: самодостаточность типа
При полном указании аргументов типа в квалифицированном имени сигнатура конструктора варианта есть результат подстановки аргументов типа в сигнатуру поля:
Result(Int, String).ok // : (Int) -> Result(Int, String)Проверка типа полезной нагрузки — это обычная проверка аргументов вызова функции (при несовпадении — ошибка E1002), новых правил вывода не вводится. Конструкторы дженериков и типов естественным образом мономорфизируются при конкретизации типа, отдельного механизма нет.
Повышение полей: имена вариантов — не поля данных
После распознавания типа как типа-суммы имена вариантов удаляются из пространства полей данных — доступ к полю по имени варианта на значении типа-суммы отвергается на этапе компиляции:
r1.ok // ошибка компиляции: ok — конструктор варианта Result, а не поле данныхПричина: значение типа-суммы в рантайме есть tagged union (см. ниже) и не несёт таблицы объявления вариантов; разрешение доступа к полю неизбежно привело бы к тихой неверной интерпретации. Это — продолжение той же дисциплины, что и RFC-011a §1.2 (единое пространство имён полей/методов, конфликт — ошибка). Для конструирования используется квалификация типа (Result.ok), для использования — деструкция через match (RFC-010b).
Представление в рантайме и равенство
Представление значения типа-суммы в рантайме — tagged union: Enum { идентичность_типа, variant_id, payload }.
- Идентичность типа несёт конкретный тип-сумму (не глобальный заполнитель) — значения разных типов-сумм несравнимы;
- variant_id нумеруется по порядку объявления вариантов в теле типа; этот же порядок служит входными данными для проверки полноты в RFC-010b;
- равенство (
==/!=): идентичность типа совпадает, variant_id совпадает, payload поэлементно равен (рекурсивно).
Миграция std
std.result переходит на механизм данного раздела для определения ok / err (нативные result_ok / result_err отходят на второй план), конструирование вариантов в std идёт по тем же правилам, что и пользовательские типы-суммы, без привилегированных каналов. Демонтаж специальных parser-обработок Result / Option в позиции типа и специальных обработок make_result по имени осуществляется в фазе 2 RFC-011b — после интерфейсизации ?Result становится обычным типом-суммой из std.
Логические операторы: and / or / ! (авторитетное определение, в стиле Zig)
Объявление определения (03.08.2026): авторитетная форма логических операторов — ключевые слова
and/or+ символьный унарный!(согласовано с таблицей приоритетов SPECsyntax.md§2.2). Данный дизайн выровнен с Zig: управление потоком с коротким замыканием — через ключевые слова, чистая унарная операция — через символ. Ранние реализации, дрейфовавшие к C с&&/||, и промежуточное ключевое словоnotудалены.
Семантика:
| Оператор | Приоритет (SPEC §2.2) | Ассоциативность | Семантика |
|---|---|---|---|
! | 3 (унарный префикс, плотное связывание) | справа налево | логическое отрицание (чистая функция, без потока управления) |
and | 10 | слева направо | короткое замыкание логического И |
or | 10 | слева направо | короткое замыкание логического ИЛИ |
# Короткое замыкание: при and слева false / при or слева true правая часть не выполняется
if x != 0 and y / x > 1 { ... } # при x == 0 деления на ноль не будет
# Плотное связывание: !a == b ≡ (!a) == b (в стиле Zig; в отличие от Python not a == b ≡ not (a == b))
!3 == 4 # false: (!3) == 4 → false
!(3 == 4) # true
!x != 0 # ≡ (!x) != 0
!list.is_empty(xs) # ≡ !(list.is_empty(xs)), отрицание после вызоваСледующие записи больше не поддерживаются (лексер сообщает об ошибке и подсказывает соответствующую форму):
x && y # ❌ удалено, используйте x and y
x || y # ❌ удалено, используйте x or y
not x # ❌ удалено, используйте !x (not снова становится обычным идентификатором; != не затрагивается)Обоснование дизайна (выровнено с Zig, ziglang/zig#272 / #6625):
- Короткое замыкание — поток управления → ключевое слово; чистая функция — операция → символ.
and/orизменяют порядок вычисления (правая часть по необходимости пропускается), что по природе аналогичноif— отсюда ключевое слово;!выполняет чистое отрицание уже вычисленного операнда, что по природе аналогично-+— отсюда символ. В YaoXiang распространение ошибки использует?(§2.11), конфликта с!нет. - Плотное связывание устраняет неоднозначность:
!визуально «прилеплен» к операнду, высокий приоритет очевиден; ключевое словоnotвынуждено отделяться пробелом от операнда, и его связывание (not a == b) вызывает мысленную неоднозначность. - Разделение понятий:
&несёт двойную нагрузку — маркер заимствования (&p/&mut p, RFC-009) и побитовое И (приоритет 8 в SPEC §2.2). Добавление&&заставит один символ нести три значения.and/or/!полностью визуально разделяют заимствование, побитовые операции и логику. - Прецедент: Zig (современный системный язык той же ниши) использует именно комбинацию ключевых слов
and/or+ символ!; Python / Lua / Ada / SQL — полностью ключевые слова (включаяnot); семейство C — полностью символы — YaoXiang берёт комбинацию Zig, получая преимущества обоих подходов. - Согласованность с Карри–Ховардом: тип как утверждение (см. раздел об изоморфизме выше), логические связки в уточняющих типах записываются как
and/or(например,{ 0 <= idx and idx < arr.len }) — естественная форма записи утверждений;!как символ унарного отрицания соответствует ¬.
Реализация:
and/orраскрываются на уровне IR в последовательность переходов с коротким замыканием (a and b ≡ if a { b } else { false }),!разбирается как плотно связывающий унарный (операнд поBP_UNARY + 1). Регрессионные тесты:tests/yaoxiang/01-syntax/basics/logical_ops.yx,logical_not.yx.
Пояснение к дизайну синтаксиса: именованные функции — синтаксический сахар лямбды
Ключевое понимание
Именованная функция и лямбда-выражение — одно и то же! Единственное отличие: именованная функция даёт лямбде имя.
// Эти две записи по сути идентичны
add: (a: Int, b: Int) -> Int = a + b // именованная функция (рекомендуется)
add: (a: Int, b: Int) -> Int = (a, b) => a + b // лямбда-форма (полностью эквивалентна)Модель синтаксического сахара
// Именованная функция = лямбда + имя
name: (Params) -> ReturnType = body
// По сути это
name: (Params) -> ReturnType = (params) => bodyКлючевой момент: когда сигнатура полностью объявляет типы параметров, имена параметров в заголовке лямбды становятся избыточными и могут быть опущены.
Правила области видимости параметров
Параметры перекрывают внешние переменные: область видимости параметров в сигнатуре перекрывает тело функции, внутренняя область имеет более высокий приоритет.
x = 10 // внешняя переменная
double: (x: Int) -> Int = x * 2 // ✅ параметр x перекрывает внешний x, результат 20Гибкость позиции аннотации
Аннотация типа может находиться в любой из следующих позиций, достаточно указать хотя бы в одной:
| Позиция аннотации | Форма | Примечание |
|---|---|---|
| Только в сигнатуре | double: (x: Int) -> Int = x * 2 | ✅ рекомендуется |
| Только в заголовке лямбды | double = (x: Int) => x * 2 | ✅ допустимо |
| В обеих позициях | double: (x: Int) -> Int = (x) => x * 2 | ✅ избыточно, но разрешено |
Полный пример
// ✅ Рекомендуется: сигнатура полная, заголовок лямбды опускается
add: (a: Int, b: Int) -> Int = a + b
inc: (x: Int) -> Int = x + 1
main: () -> Void = { print("hi") }
// ✅ Допустимо: типы аннотируются в заголовке лямбды
double = (x: Int) => x * 2
// ✅ Допустимо: аннотация в обеих позициях
double: (x: Int) -> Int = (x) => x * 2Преимущества дизайна
| Свойство | Преимущество |
|---|---|
| Краткость | При полной сигнатуре не нужно повторять имена параметров |
| Гибкость | Сохранена лямбда-форма — используйте ту, что нравится |
| Единообразие | Совпадает с шаблоном объявления переменной 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 ║
║ Это — исток YaoXiang, граница языка. ║
║ Здесь компилятор безмолвствует, философия останавливается. ║
║ ║
║ Спасибо, что достигли философской границы языка. ║
║ ║
╚══════════════════════════════════════════════════════════════╝Примечание: компилятор не может корректно обработать
Type: Type = Type(это приведёт к парадоксу вселенных Type0/Type1), но мы намеренно сохранили этот «пасхальный яйцо» — при попытке его скомпилировать вы получите дзен-сообщение от создателя языка. Это не просто техническая граница, а дань уважения YaoXiang философии типов.
Приложение
БНФ синтаксиса
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) -> (...)
# Отдельное правило БНФ не требуется — параметры : 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/ │
│ (официальный дизайн) │ │ (остаётся на месте) │
└─────────────┘ └─────────────┘