Skip to content

RFC-004: Проектирование многопозиционной юнионной привязки каррированных методов

Аннотация

Данный 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], единственный механизм привязки методов

Ссылки