Skip to content

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 typePoint: 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..., что прозрачно для пользователя.

yaoxiang
// Основной синтаксис: унификация + различие

// Переменная
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)

Мотивация

Зачем нужен этот функционал?

Текущая система типов содержит несколько разрозненных концепций:

  • Синтаксис объявления переменных
  • Синтаксис определения функций
  • Синтаксис определения типов (различные формы)
  • Синтаксис определения интерфейсов
  • Синтаксис привязки методов

Между этими концепциями отсутствует единство, что приводит к фрагментации синтаксиса и высокой стоимости обучения.

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

  1. Предельная унификация: одно синтаксическое правило охватывает все случаи
  2. Лаконичность и элегантность: симметричная эстетика name: type = value
  3. Без новых ключевых слов: повторное использование существующих синтаксических элементов
  4. Теоретическая элегантность: типы сами по себе являются значениями типа Type
  5. Дружелюбие к обобщениям: бесшовная интеграция с системой обобщений (RFC-011)

Интеграция с системой обобщений

Унифицированная модель синтаксиса RFC-010 естественно согласуется с системой обобщений RFC-011; параметры обобщений могут бесшовно интегрироваться в унифицированную модель:

yaoxiang
// Базовые обобщения (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 активно解析为代码块 { ... }, выводит как функциональный тип
yaoxiang
# ✅ Конструктор типов: есть : 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Тип TInt, Bool
Доказательство PЗначение типа T42: 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 unionMaybe: (T: Type) -> Type = { ... }

Значение name: type = value в рамках Curry-Howard:

yaoxiang
// "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 }

Почему это важно?

  1. Логическая непротиворечивость = безопасность типов: если система типов позволяет сконструировать значение типа T без какого-либо допустимого представления во время выполнения, это подобно допущению доказательства ложного суждения — система рушится. Curry-Howard говорит нам: язык с безопасной системой типов естественным образом является непротиворечивой логической системой.

  2. Уровни вселенной как необходимое условие: как подробно описано ниже, если позволить Type: Type («тип типа тоже является типом»), возникает парадокс Рассела (в теории типов проявляющийся как парадокс Жирара). Иерархия Type₀ : Type₁ : Type₂ : ... в YaoXiang гарантирует, что каждый тип принадлежит определённому уровню, формируя никогда не замыкающуюся восходящую цепочку, что фундаментально предотвращает парадоксы. Это означает, что система типов YaoXiang является логически непротиворечивой в смысле Curry-Howard.

  3. Теоретическое обоснование унифицированного синтаксиса: то, что 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. Объявление переменных

yaoxiang
// Основной синтаксис
x: Int = 42
name: String = "Alice"
flag: Bool = true

// Вывод типов (можно опустить)
y = 100  // Выводится как Int

2. Определение функций

yaoxiang
// Форма с одиночным выражением (непосредственно возвращает значение, 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
yaoxiang
# Одиночное выражение: непосредственно возвращает значение, 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 (см. правила возврата)
yaoxiang
# Управление зависимостями: b зависит от a, компилятор автоматически упорядочивает
result: Int = {
    b = a + 1      # Зависит от a → автоматически ставится после a
    a = 10         # Нет зависимостей → может выполняться первым
    return b       # Возвращает 11
}

Отличие от одиночного выражения: = expr (без фигурных скобок) — простое связывание с непосредственным возвратом значения; = { ... } (с фигурными скобками) вводит контекст вычисления с управлением зависимостями, допускающий многооператорный контекст и явный return.

Блоки spawn

spawn { ... } — единственный примитив параллелизма в YaoXiang. Он использует семантику {} с управлением зависимостями для автоматического распараллеливания:

  • Прямые дочерние присваивания внутри spawn { ... } автоматически создают параллельные задачи
  • Задачи с готовыми зависимостями выполняются немедленно и параллельно
  • Вызывающая сторона блокируется до завершения всех дочерних задач
yaoxiang
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-разрешения
yaoxiang
# Определение непрозрачного типа внутри 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: список полей, типы полей могут быть произвольными тип-выражениями.

yaoxiang
Point: Type = {
    x: Float,
    y: Float
}

Поля со значениями по умолчанию: поля могут иметь значения по умолчанию, необязательны при конструировании.

yaoxiang
Point: Type = {
    x: Float = 0,
    y: Float = 0
}

Использование:

yaoxiang
Point() → Point(x=0, y=0)
Point(x=1) → Point(x=1, y=0)
Point(x=1, y=2) → Point(x=1, y=2)

Поля без значений по умолчанию: обязательны при конструировании.

yaoxiang
Point2: Type = {
    x: Float,
    y: Float
}

Использование:

yaoxiang
Point2(x=1, y=2) //✓
Point2() //✗
Point2(x=1) //✗
Встроенные типы

Идентификаторная система YaoXiang делится на три уровня, распознаваемых последовательно разными этапами компилятора:

  1. Ключевые слова (независимый токен парсера) — ключевые слова управления потоком и объявлений, такие как if, match, pub, return
  2. Зарезервированные литералы (независимый токен парсера) — true, false, void, Type, не могут использоваться как обычные идентификаторы
  3. Встроенные имена типов (предварительно зарегистрированы в 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: непосредственная привязка внешней функции внутри определения типа

yaoxiang
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: анонимная функция + позиционная привязка

yaoxiang
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)
Реализация интерфейсов

Имя интерфейса записывается в теле типа, компилятор автоматически проверяет его реализацию

yaoxiang
Drawable: Type = {
    draw: (Surface) -> Void,
    bounding_box: () -> Rect
}

Serializable: Type = {
    serialize: () -> String
}

Point: Type = {
    x: Float,
    y: Float,
    Drawable,          # Реализует интерфейс Drawable
    Serializable      # Реализует интерфейс Serializable
}
Определение интерфейсов

Интерфейс = record type, все поля которого являются функциями

yaoxiang
Drawable: Type = {
    draw: (Surface) -> Void,
    bounding_box: () -> Rect
}

Serializable: Type = {
    serialize: () -> String
}

// Пустой тип/пустой интерфейс
EmptyType: Type = {}
Empty: Type = {}
Определение функций пространства имён

Префикс Type.name означает принадлежность к пространству имён, и только. Он не вызывает никакой неявной привязки.

yaoxiang
# Функция пространства имён: обычная функция в пространстве имён 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).

yaoxiang
// Определение функции
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] = без привязки какого-либо параметра. Пользователь должен явно решить, какие параметры заполняются вызывающей стороной.

Мультипозиционная привязка:

yaoxiang
// Привязка нескольких позиций (автоматическое каррирование)
Point.transform = transform_points[0, 1]
// Вызов: p1.transform(p2)(2.0) → transform_points(p1, p2, 2.0)

Обратная операция (метод → обычная функция):

yaoxiang
// Извлечение функции из привязки
draw_point: (p: &Point, surface: Surface) -> Void = Point.draw

4. Композиция интерфейсов

yaoxiang
// Композиция интерфейсов = пересечение типов
DrawableSerializable: Type = Drawable & Serializable

// Использование типа пересечения
process: (T: Drawable & Serializable) -> ((item: T, screen: Surface) -> String) = {
    item.draw(screen)
    return item.serialize()
}

5. Обобщённые типы

yaoxiang
// Базовые обобщения (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. Синтаксис вызова обобщений

Вызов обобщённых типов и обобщённых функций унифицированно использует синтаксис (). [] не используется ни в каком контексте обобщений.

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

  1. () выполняет всё: применение типа, вызов функции, конструирование значения — всё через ()
yaoxiang
# Аннотация типа
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) -> String
  1. Type слева, значение справа: name: type = value — параметры Type объявляются слева, справа всегда конкретное значение. Для пустого контейнера List() параметр T должен приходить из аннотации типа слева.

  2. Информация о типе пишется один раз — в объявлении параметра, компилятор несёт его дальше:

yaoxiang
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
  1. Конструирование значения выводит тип из элементов:
yaoxiang
x = List(1, 2, 3)       # Выводится как List(Int)
y = List("a", "b")      # Выводится как List(String)
z = List()              # ❌ Ошибка компиляции: T невозможно вывести
z: List(Int) = List()   # ✅ T=Int приходит из аннотации слева
  1. Псевдонимы типов:
yaoxiang
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). Старый синтаксис обобщений [] полностью удалён. [] используется только для литералов массивов/списков и индексного доступа.

Примеры

Полный пример

yaoxiang
// ======== 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])

Детальное проектирование

Алгоритм проверки интерфейсов

rust
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(())
}

Прямое присваивание интерфейсу и оптимизация времени компиляции

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

yaoxiang
// Прямое присваивание конкретного типа → конкретный тип определим во время компиляции, вызов с нулевыми накладными расходами
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

Правила:

  1. Когда правая часть является конкретным конструктором типа и определима во время компиляции, генерируется прямой вызов IR
  2. Когда тип правой части невозможно определить во время компиляции, происходит откат к механизму vtable
  3. vtable гарантирует корректность полиморфизма во время выполнения

Поддержка утиной типизации

yaoxiang
// Пока есть одинаковые методы, можно присвоить типу интерфейса
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 являются функциями, и каждая функция возвращает этот же тип, он становится типом суммы:

yaoxiang
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)
}

Обоснование дизайна:

  1. Устранение особых случаев: | — единственная форма синтаксиса в BNF, отличная от name: type = value. После удаления type_expr полностью унифицирован, парсер больше не нуждается в поддержке отдельного пути и lookahead-backtracking для variant-типов.
  2. Математическая эквивалентность: В изоморфизме Curry-Howard дизъюнкция P ⊕ Q соответствует типу суммы, который эквивалентен record type «все поля которого являются функциями, возвращающими сам тип». Оба выражают одну семантику, две системы синтаксиса не нужны.
  3. Нулевая разрушительность: До удаления | синтаксис полуподдерживался в парсере (варианты без параметров解析ровались, но типы параметров терялись при мономорфизации), никакой пользовательский код не зависел от него.
  4. Упрощение AST: Узел Type::Variant(Vec<VariantDef>) удалён, все variant-типы统一走 Type::Struct пути, все специальные ветки в downstream typecheck/mono/formatter полностью устранены.

Примечание: Семантические атрибуты типов суммы (например, исчерпывающая проверка match, размещение tagged union в памяти) выводятся typecheck-слоем из структуры Type::Struct, а не из отдельного AST-узла.

Синтаксический дизайн: именованные функции по своей сути являются синтаксическим сахаром Lambda

Ключевое понимание

Именованные функции и Lambda-выражения — это одно и то же! Единственное различие: именованная функция просто даёт Lambda имя.

yaoxiang
// Эти две формы по сути полностью идентичны
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 становятся избыточными и могут быть опущены.

Правила области видимости параметров

Параметры перекрывают внешние переменные: область видимости параметров в сигнатуре перекрывает тело функции, приоритет у внутренней области выше.

yaoxiang
x = 10  // Внешняя переменная

double: (x: Int) -> Int = x * 2  // ✅ Параметр x перекрывает внешний x, результат 20

Гибкость позиции аннотации

Аннотация типа может находиться в любой из следующих позиций, достаточно аннотировать хотя бы в одном месте:

Позиция аннотацииФормаПримечание
Только в сигнатуреdouble: (x: Int) -> Int = x * 2✅ Рекомендуется
Только в голове Lambdadouble = (x: Int) => x * 2✅ Допустимо
В обоих местахdouble: (x: Int) -> Int = (x) => x * 2✅ Избыточно, но допустимо

Полный пример

yaoxiang
// ✅ Рекомендуется: сигнатура полная, голова 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
МногословностьПолный синтаксис длиннее упрощённого, но выводится
Кривая обученияТребуется понимание унифицированной модели

Меры смягчения

yaoxiang
// 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

yaoxiang
// Попытка определить тип типа...
Type: Type = Type

Предупреждение: Это невыразимое!

╔══════════════════════════════════════════════════════════════╗
║                                                              ║
║   一生二,二生三,三生万物。                                   ║
║   易有太极,是生两仪。                                         ║
║                                                              ║
║   Type: Type = Type                                          ║
║   此乃爻象之源,语言之边界。                                   ║
║   编译器在此沉默,哲学在此驻足。                               ║
║                                                              ║
║   感谢你触达语言的哲学边界。                                   ║
║                                                              ║
╚══════════════════════════════════════════════════════════════╝

Примечание: Компилятор не может корректно обработать Type: Type = Type (это приведёт к парадоксу уровней вселенной Type0/Type1), но мы намеренно сохраняем эту «пасхалку» — при попытке компиляции вы получите дзенское сообщение от создателя языка. Это не только техническая граница, но и дань уважения философии типов YaoXiang.


Приложения

BNF-грамматика

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/     │
│(официальный │    │(оставлен на │
│   дизайн)   │    │   месте)   │
└─────────────┘    └─────────────┘