Skip to content

RFC-004: Проектное решение для многопозиционного объединённого связывания каррированных методов ​

Дополнение (2026-09-16, семантика скобок): Цепочка каррирования в данном документе имеет правило завершения, которое ранее не было записано явно. Скобки в позиции возврата объявляют: «это полный тип (значение)», и цепочка на этом завершается:

yaoxiang
f: (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.

Мотивация ​

Зачем нужна эта возможность? ​

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

  1. Негибкость позиции вызывающего: традиционное связывание может зафиксировать только obj в obj.method(args) как первый параметр.
  2. Сложность многопараметрического связывания: когда метод принимает несколько параметров одного типа, это невозможно выразить элегантно.
  3. Семантическая неоднозначность каррирования: при частичном применении трудно отличить «к какой позиции привязывать».

Цель дизайна: объединение двух парадигм ​

Данное решение нацелено на объединение функциональной и ОО-парадигм:

yaoxiang
# Функциональный взгляд: явная передача всех аргументов
distance(p1, p2)

# ОО-взгляд: неявный this
p1.distance(p2)

# Синтаксический сахар [positions] делает обе записи эквивалентными, по сути это вызов функции
Point.distance = distance[0]   # this привязывается к позиции 0

Ключевые достоинства:

  • Внизу — функция, наверху — синтаксис метода
  • Без ключевого слова self, сохраняя лаконичность языка
  • Полная функциональность: вызов метода по сути является передачей аргументов
  • [0], [1], [-1] гибко управляют позицией привязки this
  • Единый синтаксис: определение функции использует формат name: (params) -> Return = body

Текущие проблемы ​

yaoxiang
# Проблемы существующего дизайна:
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].

Однопозиционное связывание ​

yaoxiang
# Явное связывание с первым параметром типа 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] — нет связывания:

yaoxiang
# Нет [n] → просто псевдоним пространства имён, без синтаксиса вызова через .
Point.distance = distance            # доступно только Point.distance(p1, p2)
# p1.distance(p2)  ❌  связывание отсутствует

# Фабричные функции естественно легальны, без специальной обработки
create_point: () -> Point = { ... }
Point.create = create_point          # Point.create()   ✅
  • Типобезопасность: связывание только при совпадении типов, что предотвращает ошибки
  • Гибкое управление: [n] точно контролирует позицию связывания

Каррированное связывание ​

Когда количество параметров функции больше, чем число позиций связывания, автоматически порождается каррированная функция. Связывание всегда является явной операцией.

yaoxiang
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] для точного управления связью между аргументами функции и типом:

yaoxiang
# Формат: 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

Подробное синтаксическое определение ​

объявление_связывания ::= тип '.' идентификатор '=' имя_функции '[' список_позиций ']'

список_позиций      ::= позиция (',' позиция)*
позиция             ::= целое_число          # позиционный аргумент
                     | '_'                   # пропуск позиции (заполнитель)
                     | целое_число '..' целое_число   # диапазон позиций (будущее расширение)

имя_функции         ::= идентификатор
тип                 ::= идентификатор (泛型_аргументы)?

Встроенное связывание ​

Связывание может быть записано прямо в теле определения типа, без отдельного оператора связывания:

yaoxiang
# Способ 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 заполняется вызывающим)

Примеры использования ​

yaoxiang
# === Полный пример ===

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)

Поддержка деструктуризации кортежей ​

yaoxiang
# === Связывание с деструктуризацией кортежа ===

# Функция принимает параметр-кортеж
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))

Связывание функций с множественным возвратом ​

yaoxiang
# === Связывание функций с множественным возвратом ===

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

Детальный дизайн ​

Реализация в компиляторе ​

Правила проверки типов ​

rust
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 = distancePoint.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. Фаза 1: Базовое связывание (v0.3)

    • Реализация синтаксиса однопозиционного связывания [n] (n с 0, поддержка отрицательных)
    • Базовая проверка типов и кодогенерация
    • Покрытие модульными тестами
  2. Фаза 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] — единственный механизм привязки методов

Ссылки ​