RFC-004: Проектное решение для многопозиционного объединённого связывания каррированных методов
Дополнение (2026-09-16, семантика скобок): Цепочка каррирования в данном документе имеет правило завершения, которое ранее не было записано явно. Скобки в позиции возврата объявляют: «это полный тип (значение)», и цепочка на этом завершается:
yaoxiangf: (a: Int) -> (b: Int) -> Int # каррирование: два сегмента параметров, f(1)(2) g: (a: Int) -> ((b: Int) -> Int) # возврат функции: один сегмент параметров, g(1) даёт функциюОбоснование: аннотация есть тип.
gобъявляетg(1) : (b: Int) -> Int, значитg(1)должно быть этой функцией (а не «следующим сегментом параметров»). Это также соответствует замыслу RFC-010 и спецификации языка, гдеmap: (T: Type) -> ((list: List(T), f: (x: T) -> R) -> List(R))записано именно так, и согласуется с сигнатурами методов вstdlib.md:-> ((self: ...) -> ...).Рефлексивно: вложенные
Fnбез скобок всегда означают каррирование (включая(T: Type) -> (x: T) -> Tиз RFC-011). Оба поведения различимы и оба допустимы.Реализационно:
Type::Parenпорождается только тогда, когда «скобки не образуют группу параметров» (то есть после скобок не следует->), и воспринимается только разделителем цепочки и форматтером; в остальных местах он прозрачен.
Резюме
Данный RFC предлагает совершенно новый синтаксис многопозиционного объединённого связывания, позволяющий точно привязывать функцию к любой позиции параметров типа, поддерживая как однопозиционное, так и многопозиционное объединённое связывание, что фундаментально решает проблему «кто является вызывающим» в каррированном связывании без введения ключевого слова self.
Мотивация
Зачем нужна эта возможность?
В текущем дизайне языка привязка свободной функции в качестве метода типа сталкивается со следующими проблемами:
- Негибкость позиции вызывающего: традиционное связывание может зафиксировать только
objвobj.method(args)как первый параметр. - Сложность многопараметрического связывания: когда метод принимает несколько параметров одного типа, это невозможно выразить элегантно.
- Семантическая неоднозначность каррирования: при частичном применении трудно отличить «к какой позиции привязывать».
Цель дизайна: объединение двух парадигм
Данное решение нацелено на объединение функциональной и ОО-парадигм:
# Функциональный взгляд: явная передача всех аргументов
distance(p1, p2)
# ОО-взгляд: неявный this
p1.distance(p2)
# Синтаксический сахар [positions] делает обе записи эквивалентными, по сути это вызов функции
Point.distance = distance[0] # this привязывается к позиции 0Ключевые достоинства:
- Внизу — функция, наверху — синтаксис метода
- Без ключевого слова
self, сохраняя лаконичность языка - Полная функциональность: вызов метода по сути является передачей аргументов
[0],[1],[-1]гибко управляют позицией привязки this- Единый синтаксис: определение функции использует формат
name: (params) -> Return = body
Текущие проблемы
# Проблемы существующего дизайна:
Point: Type = { x: Float, y: Float }
Vector: Type = { x: Float, y: Float, z: Float }
distance: (a: Point, b: Point) -> Float = { ... }
transform: (p: Point, v: Vector) -> Point = { ... }
# Можно привязать только к первому параметру
Point.distance = distance # эквивалентно distance[0]
# p1.distance(p2) → distance(p1, p2) ✓
# Но что если сигнатура transform: transform(Vector, Point)?
# Невозможно выразить семантику p1.transform(v1) → transform(v1, p1)Предложение
Ключевая идея: явное указание позиции
Ключевое правило: без [n] — нет связывания. Point.name = func — лишь псевдоним в пространстве имён, не вызывающий никакого неявного связывания. Чтобы синтаксис вызова через . работал как p.name(args), требуется явное указание: Point.name = func[n].
Однопозиционное связывание
# Явное связывание с первым параметром типа Point (индексация с 0)
Point.distance = distance[0]
p1.distance(p2) # → distance(p1, p2)
# Связывание со вторым параметром типа Point
Point.compare = distance[1] # связывание со вторым параметром Point
p1.compare(p2) # → distance(p2, p1)Без [n] — нет связывания:
# Нет [n] → просто псевдоним пространства имён, без синтаксиса вызова через .
Point.distance = distance # доступно только Point.distance(p1, p2)
# p1.distance(p2) ❌ связывание отсутствует
# Фабричные функции естественно легальны, без специальной обработки
create_point: () -> Point = { ... }
Point.create = create_point # Point.create() ✅- Типобезопасность: связывание только при совпадении типов, что предотвращает ошибки
- Гибкое управление:
[n]точно контролирует позицию связывания
Каррированное связывание
Когда количество параметров функции больше, чем число позиций связывания, автоматически порождается каррированная функция. Связывание всегда является явной операцией.
Point: Type = { x: Float, y: Float }
# Базовая функция
scale: (p: Point, factor: Float) -> Point = {
return Point(p.x * factor, p.y * factor)
}
# Явное связывание к позиции 0 → каррирование: оставшийся параметр factor предоставляется вызывающим
Point.scale = scale[0]
# Вызов
p1 = Point(2.0, 3.0)
scaled = p1.scale(2.0) # → scale(p1, 2.0)
# Цепочечный вызов выглядит ещё элегантнее
result = Point(2.0, 3.0).scale(2.0) # → Point(4.0, 6.0)Синтаксис индексов позиций
Вводится синтаксис [position] для точного управления связью между аргументами функции и типом:
# Формат: Type.method = function[positions]
# === Базовое связывание ===
# Однопозиционное связывание
Point.distance = distance[1] # связывание с 1-м параметром (индексация с 0)
# Использование: p1.distance(p2) → distance(p2, p1)
# Многопозиционное объединённое связывание (деструктуризация кортежа)
Point.transform = transform[1, 2] # связывание с 1-м и 2-м параметрами
# Использование: p1.transform(v1) → transform(v1, p1)
# Исходная сигнатура: transform(Point, Vector) → Point
# После связывания: Point.transform(Vector) → PointПодробное синтаксическое определение
объявление_связывания ::= тип '.' идентификатор '=' имя_функции '[' список_позиций ']'
список_позиций ::= позиция (',' позиция)*
позиция ::= целое_число # позиционный аргумент
| '_' # пропуск позиции (заполнитель)
| целое_число '..' целое_число # диапазон позиций (будущее расширение)
имя_функции ::= идентификатор
тип ::= идентификатор (泛型_аргументы)?Встроенное связывание
Связывание может быть записано прямо в теле определения типа, без отдельного оператора связывания:
# Способ 1: связывание непосредственно в теле определения типа
Point: Type = {
x: Float = 0,
y: Float = 0,
distance = distance[0] # связывание с позицией 0
}
# Способ 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]Семантика каррирования:
- При связывании
distance = distance[0]исходная сигнатура(a: Point, b: Point) -> Float - Порождаемая сигнатура метода:
b: Point -> Float(позиция 0 заполняется вызывающим)
Примеры использования
# === Полный пример ===
Point: Type = { x: Float, y: Float }
Vector: Type = { x: Float, y: Float, z: Float }
# 1. Базовая функция расстояния
distance: (a: Point, b: Point) -> Float = {
dx = a.x - b.x
dy = a.y - b.y
return (dx * dx + dy * dy).sqrt()
}
# Связывание: Point.distance = distance[1]
# Вызов: p1.distance(p2) → distance(p2, p1)
# Но мы хотим p1.distance(p2) → distance(p1, p2), поэтому:
Point.distance = distance[0]
# 2. Операция трансформации (многопозиционное связывание)
transform: (p: Point, v: Vector) -> Point = {
return Point(p.x + v.x, p.y + v.y)
}
# Связывание Point.transform = transform[1]
# Вызов: p.transform(v) → transform(v, p) ❌
# Связывание Point.transform = transform[0]
# Вызов: p.transform(v) → transform(p, v) ✓
# 3. Сложная функция с многими параметрами
multiply: (a: Point, s: Float) -> Point = {
return Point(a.x * s, a.y * s)
}
# Связывание только 1-го параметра (тип Point), с сохранением 3-го параметра
Point.scale = multiply[0, _]
# Вызов: p.scale(2.0) → multiply(p, 2.0)
# 4. Межтиповое связывание
Circle: Type = { center: Point, radius: Float }
distance: (a: Circle, b: Circle) -> Float = {
return a.center.distance(b.center) - a.radius - b.radius
}
# Связывание метода расстояния с типом Circle
Circle.distance = distance[0, 1]
# Вызов: c1.distance(c2) → distance(c1, c2)Поддержка деструктуризации кортежей
# === Связывание с деструктуризацией кортежа ===
# Функция принимает параметр-кортеж
process_coordinates: (coord: (Float, Float)) -> String = {
return match coord {
(0.0, 0.0) -> "origin"
(x, 0.0) -> "on x-axis at ${x}"
(0.0, y) -> "on y-axis at ${y}"
(x, y) -> "point at (${x}, ${y})"
}
}
Coord: Type = { x: Float, y: Float }
# Автоматическое связывание с деструктуризацией: Coord -> (Float, Float)
Coord.describe = process_coordinates[1]
# Использование: coord.describe() → process_coordinates((coord.x, coord.y))Связывание функций с множественным возвратом
# === Связывание функций с множественным возвратом ===
min_max: (list: List(Int)) -> (Int, Int) = {
min = list.reduce(Int.MAX, (a, b) => if a < b then a else b)
max = list.reduce(Int.MIN, (a, b) => if a > b then a else b)
return (min, max)
}
List.range: (T:Type)->((self: List(T)) -> (T, T)) = min_max[1]
# Использование: (min_val, max_val) = list.range()Детальный дизайн
Реализация в компиляторе
Правила проверки типов
fn check_binding_type_compatibility(
binding: &Binding,
func: &Function
) -> Result<(), TypeError> {
// 1. Если позиция ищется автоматически (не указана явно), проверить, найдено ли совпадение
if binding.positions.is_empty() {
return Err(TypeError::NoMatchingParameter(
binding.type_name.clone(),
func.name.clone()
));
}
// 2. Проверить валидность всех индексов позиций
for pos in &binding.positions {
if *pos >= func.params.len() {
return Err(TypeError::InvalidBindingPosition(*pos));
}
}
// 3. Проверить совместимость типов в позициях связывания
for pos in &binding.positions {
let param_type = &func.params[*pos].type_;
let binding_type = &binding.type_name;
if !isAssignable(binding_type, param_type) {
return Err(TypeError::IncompatibleTypes(
binding_type, param_type
));
}
}
// 4. Проверить соответствие аргументов вызова метода оставшимся параметрам
Ok(())
}Поведение во время выполнения
| Сценарий | Синтаксис связывания | Вызов | Преобразуется в |
|---|---|---|---|
| Без связывания | Point.distance = distance | Point.distance(p1, p2) | distance(p1, p2) |
| Однопозиционное | Point.distance = distance[0] | p1.distance(p2) | distance(p1, p2) |
| Однопозиционное | Point.distance = distance[1] | p1.distance(p2) | distance(p2, p1) |
| Отрицательный индекс | Point.test = func[-1] | p.test(a, b) | func(a, b, p) |
| Многопозиционное (каррирование) | Point.scale = scale[0] | p.scale(2.0) | scale(p, 2.0) |
| Заполнитель | Type.method = func[1] | obj.method(arg) | func(arg, obj) |
Пояснения:
- Без связывания:
Point.name = func— лишь псевдоним пространства имён, без синтаксиса вызова через. [0]: вызывающий привязывается к позиции 0 (первый аргумент)[1]: вызывающий привязывается к позиции 1 (второй аргумент)[-1]: вызывающий привязывается к последней позиции (отсчёт с конца)
Компромиссы
Достоинства
- Явное связывание:
[n]— единственный механизм связывания; без указания связывания нет, нет неявного поведения - Точный контроль: можно привязывать к любой позиции аргумента, высокая гибкость
- Типобезопасность: полная проверка типов на этапе компиляции, связывание только при совпадении типов
- Лаконичный синтаксис:
[position]интуитивно понятен - Без ключевого слова
self: сохраняется лаконичность языка - Дружелюбность к каррированию: естественная поддержка частичного применения и цепочечных вызовов
- Дружелюбность к ООП: автоматическое каррирование упрощает миграцию ОО-программистов
Недостатки
- Кривая обучения: требуется понимание концепции индексов позиций
- Сложность компиляции: разрешение связывания и проверка типов усложняют компилятор
- Сложность отладки: сообщения об ошибках должны чётко указывать на проблемы с позицией связывания
Альтернативы
| Решение | Описание | Почему не выбрано |
|---|---|---|
Ключевое слово self | Ввести self в стиле Python/Rust | Противоречит философии дизайна YaoXiang без неявного self |
| Связывание по именованным аргументам | Использовать именованные аргументы func(a=obj) | Требует изменения определения сигнатур функций, увеличивает сложность |
| Система макросов | Реализовать связывание через макросы | Большие накладные расходы во время выполнения, сниженная типобезопасность |
| Перегрузка операторов | Ограничить self определёнными позициями | Непоследовательный синтаксис, семантическая путаница |
Стратегия реализации
Этапы
Фаза 1: Базовое связывание (v0.3)
- Реализация синтаксиса однопозиционного связывания
[n](n с 0, поддержка отрицательных) - Базовая проверка типов и кодогенерация
- Покрытие модульными тестами
- Реализация синтаксиса однопозиционного связывания
Фаза 2: Расширенные возможности (v0.5)
- Поддержка синтаксиса диапазонов
[n..m] - Оптимизация вычисления позиций на этапе компиляции
- Поддержка синтаксиса диапазонов
Зависимости
- Нет внешних зависимостей
- Прямой связи с RFC-001 (обработка ошибок) нет
- Может быть реализовано независимо
Риски
- Обеспечение совместимости с существующим синтаксисом связывания
- Стратегия оптимизации производительности (раскрытие на этапе компиляции vs. поиск во время выполнения)
Открытые вопросы
Следующие вопросы уже решены в дизайне и зафиксированы в Приложении A:
Начало индексации с 0→ Решено: с 0Отрицательные индексы→ Решено: поддерживаютсяЗаполнитель→ Решено: использовать_Синтаксис диапазонов→ Решено: реализовать
Оставшиеся открытые вопросы:
- [ ] Обработка совместимости с существующим синтаксисом связывания
- [ ] Стратегия оптимизации производительности (раскрытие на этапе компиляции vs. поиск во время выполнения)
Приложения
Приложение A: Запись проектных решений
| Решение | Решение | Обоснование |
|---|---|---|
| Базис индексации | С 0 | Согласуется с индексацией кортежей/списков аргументов |
| Отрицательные индексы | Поддерживаются | Гибкость, отсчёт с конца |
| Заполнитель | _ | Лаконично, универсальный символ |
| Синтаксис диапазонов | Реализуется | Пакетное связывание, например [0..2] |
| Стиль синтаксиса | Инфиксный Type.method = func[positions] | Согласовано с RFC-010 |
| Правило связывания | Только явное [n] привязывает, без указания — не привязывает | Нет неявного поведения, определение функции и связывание ортогональны |
| Пространство имён | Type.name — лишь принадлежность к пространству имён, не вызывает связывание | Разделение определения и связывания |
| Синтаксис функций | Имена параметров в сигнатуре: name: (params) -> Return | Согласовано с RFC-010 |
Приложение B: Глоссарий
| Термин | Определение |
|---|---|
| Позиция связывания | Индекс позиции в списке аргументов функции |
| Объединённое связывание | Привязка типа к нескольким позициям аргументов |
| Частичное применение | Предоставление лишь части аргументов, возврат функции, ожидающей завершения вызова |
| Единый синтаксис | name: (params) -> Return = body, имена параметров объявляются в сигнатуре |
| Функция в пространстве имён | Синтаксис Type.name — функция принадлежит пространству имён Type, без скрытого связывания |
| Явное связывание | Type.name = func[n] — единственный механизм привязки методов |
