Skip to content

RFC-007: Унифицированный синтаксис определения функций ​

Связано: В точке пересечения позиций «простейшей записи без параметров» name = { ... } и синтаксиса «блока со значением» из RFC-010, решение состоит в том, что содержимое определяет тип — => всегда означает функцию, аннотация Fn означает функцию, аннотация не-Fn означает значение блока, при отсутствии аннотации тип выводится по содержимому. См. Приложение D в RFC-010a.

Связанное дополнение: Правила завершения операторов и переноса строк в теле функции (блок кода { ... }) (явное разделение ;, завершение переводом строки, исключения для продолжения строки) определены в RFC-038 (черновик); настоящий RFC их не затрагивает.

Краткое содержание ​

Настоящий RFC определяет окончательное решение для синтаксиса определения функций языка YaoXiang. Используется унифицированный синтаксис name: (params) -> Return = body, полностью согласующийся с моделью name: type = value из RFC-010.

Во избежание неоднозначности: когда функция имеет входные параметры, типы параметров должны быть явно указаны как минимум в одном из мест — в «сигнатуре» или в «заголовке lambda»; если оба места опущены, это отвергается.

Значение блока кода { ... } определяется хвостовым выражением; return — нелокальный выход типа Never (см. RFC-010a). Форма выражения = expr задаёт значение напрямую.

Мотивация ​

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

  1. Согласованность синтаксиса: Устранение исторического наследия старого синтаксиса, унификация стиля
  2. Краткость: Алгоритм HM автоматически выводит типы, сокращая шаблонный код
  3. Безопасность типов: Алгоритм HM гарантирует безопасность типов, явные аннотации только когда вывод невозможен
  4. Зрелость языка: Алгоритм HM — зрелое решение современных функциональных языков

Модель унифицированного синтаксиса ​

Ключевой принцип: name: Signature = LambdaBody

  • Полная форма: Сигнатура (включая имена параметров + типы + -> + тип возврата) + Заголовок lambda (включая имена параметров)
  • Правила сокращения: Опускать, когда это не вносит неоднозначности
    • -> не может быть опущен (признак function type, иначе будет разобрано как tuple)
    • При наличии входных параметров типы параметров должны быть явно указаны как минимум в одном из мест — в сигнатуре или в заголовке lambda
    • Заголовок lambda может быть опущен → если сигнатура уже объявляет имена и типы параметров
    • Тип возврата может быть указан явно или опущен, когда он может быть выведен
yaoxiang
# Полная форма (сигнатура полная + заголовок lambda полный)
add: (a: Int, b: Int) -> Int = (a, b) => a + b

# Сокращение: опускаем заголовок lambda (сигнатура уже объявляет параметры)
add: (a: Int, b: Int) -> Int = a + b

# Сокращение: опускаем сигнатуру (заголовок lambda задаёт типы параметров)
add = (a: Int, b: Int) => a + b

# ❌ Ошибка: типы параметров не указаны ни в одном из мест
# add = (a, b) => a + b

Цели дизайна ​

yaoxiang
# === Полная форма ===
add: (a: Int, b: Int) -> Int = (a, b) => { a + b }

# === Сокращённые формы ===
add: (a: Int, b: Int) -> Int = a + b                 # Опускаем заголовок lambda
add = (a: Int, b: Int) => a + b                      # Опускаем сигнатуру

# === Функции без параметров ===
main: () -> Void = () => { println("Hello") }          # Полная форма
main: () -> Void = { println("Hello") }                # Опускаем заголовок lambda
main: () -> Void = { println("Hello") }                            # Простейшая форма (выводится как () -> Void)

# === Generic-функции (используется унифицированный синтаксис RFC-010) ===
identity: (T: Type) -> ((x: T) -> T) = (x) => x         # Полная форма
identity: (T: Type) -> ((x: T) -> T) = x                # Опускаем заголовок lambda
identity = (x: T) => x                                  # Опускаем сигнатуру (заголовок lambda задаёт типы)

# === Рекурсивные функции ===
factorial: (n: Int) -> Int = (n) => {
    if n <= 1 { return 1 } else { return n * factorial(n - 1) }
}

Правила синтаксиса ​

СценарийСинтаксисОписание
Полная формаname: (a: Type, b) -> Ret = (a, b) => { return ... }Сигнатура + заголовок lambda полные
Опускаем заголовок lambdaname: (a: Type, b: Type) -> Ret = { ... }Сигнатура уже объявляет параметры
Опускаем сигнатуруname = (a: Type, b: Type) => { ... }заголовок lambda задаёт типы параметров
Полная форма без параметровname: () -> Void = () => { return ... }Полная функция без параметров
Сокращённая форма без параметровname: () -> Void = { return ... }Опускаем заголовок lambda
Простейшая форма без параметровname = { return ... }Простейшая форма без параметров и без возврата

Внимание: Значение блока кода { ... } определяется хвостовым выражением (единственный выход); return — нелокальный выход типа Never, выходящий из ближайшей границы функции. Форма выражения = expr задаёт значение напрямую. Подробности см. в RFC-010a.

Внимание: -> — признак function type, не может быть опущен (иначе будет разобрано как tuple).

Важно: Выражение if использует фигурные скобки {} для обёртывания ветвей, ключевые слова then/else не поддерживаются:

yaoxiang
# Правильно: используем фигурные скобки
if n <= 1 { return 1 } else { return n * factorial(n - 1) }

# Неправильно: ключевые слова then/else не поддерживаются
# if n <= 1 then return 1 else return n * factorial(n - 1)

Предложение ​

Поддержка алгоритма HM и полиморфизма высшего ранга ​

Ключевая возможность: Алгоритм HM поддерживает полиморфизм высшего ранга (Higher-rank polymorphism) через аннотации generic-типов

Принципы дизайна:

  • Функции высшего порядка: При передаче функций в качестве аргументов необходимы generics для ограничения function type
  • Форма аннотации типа: (T: Type) -> ((f: (T) -> T, x: T) -> T) — параметр-генерик ограничивает function type
  • Рабочий процесс HM: Вывод function type через параметры-генерики, реализация композиции полиморфных функций

Примеры:

yaoxiang
# ✅ Поддерживается полиморфизм высшего ранга: генерик ограничивает параметр типа функции
call_twice: (T: Type) -> ((f: (T) -> T, x: T) -> T) = {
    return f(f(x))
}
# Использование: call_twice((x) => x + 1, 5)  # вывод T=Int

compose: (A: Type, B: Type, C: Type) -> ((f: (B) -> C, g: (A) -> B, x: A) -> C) = {
    return f(g(x))
}
# Использование: compose((x) => x * 2, (x) => x + 1, 5)  # вывод A=Int, B=Int, C=Int

# ❌ Не поддерживается: функция высшего порядка без ограничения-генерика
# bad_hof: (f, x) => f(f(x))  # HM не может вывести тип, отсутствует параметр-генерик

Процесс вывода HM:

  1. Идентифицировать параметры функций высшего порядка: f: (T) -> T
  2. Создать ограничение-генерик: (T: Type)
  3. Вывести конкретные типы через инстанциацию генериков
  4. Реализовать композицию полиморфных функций

Правила синтаксиса lambda-выражений ​

Важное правило: Значение блока кода { ... } определяется хвостовым выражением (единственный выход); return — нелокальный выход типа Never, выходящий из ближайшей границы функции. Форма выражения = expr задаёт значение напрямую. Подробности см. в RFC-010a.

Форма синтаксисаСинтаксисВыход значения
Форма блока{ statements }Хвостовое выражение (пустой блок {} — Void)
Форма выраженияexpressionЗначение выражения
returnreturn eНелокальный выход из функции, тип Never

Примеры:

yaoxiang
main: () -> Void = { println("Hello") }         # Хвостовое выражение имеет тип Void
add: (a: Int, b: Int) -> Int = { a + b }        # Хвостовое выражение задаёт значение
empty: () -> Void = {}                          # Пустой блок → Void

# Досрочный возврат: использование return
factorial: (n: Int) -> Int = {
    if n <= 1 { return 1 }
    return n * factorial(n - 1)
}

# Форма выражения: задаёт значение напрямую
add: (a: Int, b: Int) -> Int = a + b            # Правильно: форма выражения
main: () -> Void = println("Hello")               # Правильно: форма выражения

Основные идеи:

  1. Определение функций использует алгоритм HM для вывода типов — максимальный вывод, явная ошибка при невозможности вывода
  2. Принцип работы алгоритма HM: Автоматический вывод типов через контекстную информацию, такую как ограничения типов операторов и отношения вызовов функций
  3. Поддержка generics: Полиморфные функции используют синтаксис generics (T: Type) для явного ограничения параметров типа (RFC-010/011)
  4. Границы вывода: Тип возврата и локальные переменные могут быть выведены; типы параметров функций с параметрами должны быть указаны явно (в сигнатуре или в заголовке lambda)
  5. Функции без параметров и без возврата используют name: () -> Void = { ... }, согласовано с RFC-010
  6. Старый синтаксис выводится из употребления, предоставляются инструменты миграции

Примеры вывода типов:

yaoxiang
# Generic-функция: явный параметр типа (используется унифицированный синтаксис RFC-010)
identity: (T: Type) -> ((x: T) -> T) = x
map: (T: Type, R: Type) -> ((f: (T) -> R, list: List(T)) -> List(R)) = {
    result = List(R)()
    for item in list { result.push(f(item)) }
    return result
}

# Полиморфные функции: определяются через явные ограничения-генерики (RFC-010/011)
add: (T: Add) -> ((a: T, b: T) -> T) = a + b
print_sum: (a: Int, b: Int) -> Void = { println(a + b) }  # выводится как (Int, Int) -> Void

# Полиморфизм высшего ранга: реализуется через аннотации generic-типов для поддержки HM
call_twice: (T: Type) -> ((f: (T) -> T, x: T) -> T) = { return f(f(x)) }
compose: (A: Type, B: Type, C: Type) -> ((f: (B) -> C, g: (A) -> B, x: A) -> C) = { return f(g(x)) }
yaoxiang
# === Определение функций: вывод типов алгоритмом HM ===

# Стандартная функция: алгоритм HM выводит тип возврата (типы параметров должны быть явными)
add = (a: Int, b: Int) => a + b            # выводится как (a: Int, b: Int) -> Int
main: () -> Void = { println("Hello") }                # выводится как () -> Void

# Частично явные параметры: алгоритм HM выводит оставшиеся части
print_sum: (a: Int, b: Int) -> Void = { println(a + b) }  # выводится как (Int, Int) -> Void
greet: (name: String) -> Void = { println("Hello " + name) }  # выводится как (String) -> Void

# Generic-функция: явное ограничение параметра полиморфного типа (используется унифицированный синтаксис RFC-010)
identity: (T: Type) -> ((x: T) -> T) = x
map: (T: Type, R: Type) -> ((f: (T) -> R, list: List(T)) -> List(R)) = {
    # реализация функции map
    return List(R)()
}

# Рекурсивная функция: выводится алгоритмом HM и ограничениями рекурсии
factorial: (n: Int) -> Int = {
    if n <= 1 { return 1 } else { return n * factorial(n - 1) }
}

# === Присваивание переменных: вывод типов алгоритмом HM ===

# Явный тип
x: Int = 42

# Алгоритм HM автоматически выводит Int
y = 42                               # выводится как Int

# Алгоритм HM автоматически выводит String
name = "YaoXiang"                    # выводится как String

# Алгоритм HM автоматически выводит Float
pi = 3.14159                         # выводится как Float

Правила вывода типов HM:

СценарийСинтаксисОпускаемая частьПример
Полная формаname: (a: Type, b: Type) -> Ret = (a, b) => ...НетСигнатура + заголовок lambda полные
Опускаем заголовок lambdaname: (a: Type, b: Type) -> Ret = ...Заголовок lambdaСигнатура уже объявляет параметры
Опускаем сигнатуруname = (a: Type, b: Type) => ...СигнатураЗаголовок lambda задаёт типы параметров
Опускаем тип возврата Retname: (a: Type, b: Type) -> = ...Тип возвратаHM выводит тип возврата
Полная форма без параметровname: () -> Void = () => { ... }НетПолная функция без параметров
Сокращённая форма без параметровname: () -> Void = { ... }Заголовок lambdaОпускаем () =>
Простейшая форма без параметровname = { ... }ВсёПростейшая форма без параметров и без возврата
Присваивание переменнойname = valueТипHM выводит тип
Явная переменнаяname: Type = valueНетЯвная аннотация типа

Ключевые принципы:

  • -> — признак function type, не может быть опущен (иначе будет разобрано как tuple)
  • Тип возврата Ret может быть опущен, HM выводит его из тела функции
  • При наличии входных параметров типы параметров должны быть явно указаны (в сигнатуре или в заголовке lambda)
  • Остальные части могут быть опущены, когда они могут быть выведены и это не вносит неоднозначности
  • Никаких неявных преобразований типов, чтобы избежать хаоса в стиле JavaScript

Подробный дизайн ​

Раскрытие синтаксического сахара ​

Независимо от сокращений, всё в итоге нормализуется к унифицированному промежуточному представлению:

rust
// Полная форма
add: (a: Int, b: Int) -> Int = (a, b) => a + b

// Раскрытый IR
let add: (Int, Int) -> Int = |a: Int, b: Int| -> Int {
    return a + b
};

// Опускаем заголовок lambda
add: (a: Int, b: Int) -> Int = a + b

// Раскрытый IR (идентичен полной форме)
let add: (Int, Int) -> Int = |a: Int, b: Int| -> Int {
    return a + b
};

// Опускаем сигнатуру (заголовок lambda задаёт типы параметров)
add = (a: Int, b: Int) => a + b

// Раскрытый IR
let add: (Int, Int) -> Int = |a: Int, b: Int| -> Int {
    a + b
};

Определение синтаксиса ​

bnf
function_def ::= identifier ':' type_expr '=' expression
               | identifier '=' expression
               | identifier '=' block                    # Простейшая форма: без параметров и без возврата

identifier ::= [a-zA-Z_][a-zA-Z0-9_]*

type_expr ::= identifier                     # Ссылка на тип
       | '()'                          # Пустой тип
       | '(' parameters ')' '->' type_expr   # function type (имена параметров в сигнатуре)
       | type_expr '->' type_expr            # Простой function type
       | identifier '(' type_expr (',' type_expr)* ')'  # Применение типа

expression ::= '(' parameters ')' '=>' block
             | '(' ')' '=>' block
             | '(' parameters ')' '=>' expression

parameters ::= parameter (',' parameter)*
parameter ::= identifier                # type inference
            | identifier ':' type_expr      # Частично явный тип

block ::= '{' statement (',' statement)* '}'
        | expression

statement ::= identifier ':' expression  # Оператор присваивания
           | expression                  # Оператор-выражение (выполняется, но не возвращается)
           | 'return' expression         # Оператор возврата (возвращает указанное значение)

# Внимание: в блоке кода необходимо использовать return для возврата значения; при отсутствии return по умолчанию возвращается Void
# Например: { return 1 + 1 } возвращает Int; { println("Hello") } возвращает Void
# Внимание: параметры-генерики используют синтаксис (T: Type), как часть function type, отдельное правило BNF не требуется

Обработка ошибок ​

yaoxiang
# === Примеры ошибок компиляции ===

# Ошибка 1: несоответствие возвращаемого типа блока кода
add: (a: Int, b: Int) -> Int = { println(a + b) }
// Ошибка: в блоке нет return, по умолчанию возвращается Void, но сигнатура ожидает Int
// Правильно: add: (a: Int, b: Int) -> Int = a + b
// Или: add: (a: Int, b: Int) -> Int = { return a + b }

# Ошибка 2: использование необъявленного параметра типа
identity: (x: T) -> T = x
// Ошибка: T не объявлен; требуется явный параметр-генерик (RFC-010)
// Правильно: identity: (T: Type) -> ((x: T) -> T) = x

# Правильно: алгоритм HM выводит тип возврата
double = (x: Int) => x + x

# Полная форма (пошаговое сокращение)
double: (x: Int) -> Int = (x) => x + x                # Полная
double: (x: Int) -> Int = x + x                       # Опускаем заголовок lambda
double = (x: Int) => x + x                            # Опускаем тип возврата (HM выводит)
// double = (x) => x + x                               # ❌ Типы параметров не могут быть опущены в обоих местах

Компромиссы ​

Преимущества ​

  • Согласованность синтаксиса: Модель name: Signature = LambdaBody покрывает все сценарии
  • Гибкие сокращения: Любая часть может быть опущена, когда HM может её вывести
  • Безопасность типов: Алгоритм HM гарантирует безопасность типов, избегая неявных преобразований
  • Поддержка рекурсии: Алгоритм HM и ограничения рекурсии автоматически выводят типы
  • Нулевая нагрузка: Плавный переход от полной формы к простейшей

Недостатки ​

  • Стоимость миграции: Старый код требует инструментов миграции
  • Стоимость обучения: Требуется понимание модели «полная форма + произвольные сокращения»

Альтернативы ​

ПодходОписаниеПочему не выбран
Вывод типов алгоритмом HMИспользование алгоритма Хиндли-Милнера для вывода типов✅ Принято, стандарт современных функциональных языков
Явное объявление типовВсе типы должны быть записаны явноНарушает принцип упрощения синтаксиса, увеличивает шаблонный код
Сохранение старого синтаксисаОдновременная поддержка нового и старого синтаксисаРазделение синтаксиса, высокие затраты на сопровождение
Ключевое слово fnВведение fn для различения функций и переменныхНарушает принцип дизайна «функция — это lambda»

Стратегия реализации ​

Фазы ​

  1. Фаза 1: Синтаксический разбор и алгоритм HM (v0.3)

    • Реализовать новый синтаксис name = lambda + вывод типов алгоритмом HM
    • Реализовать заполнение по умолчанию для функций без параметров и без возврата
  2. Фаза 2: Инструменты миграции (v0.3)

    • Разработать инструмент yaoxiang-migrate --old-to-new
    • Автоматическое преобразование кода со старым синтаксисом
  3. Фаза 3: Валидация и документация (v0.3)

    • Проверка завершённости миграции старого кода
    • Обновление документации

Инструмент миграции ​

bash
# Миграция одного файла
yaoxiang-migrate --old-to-new src/main.yaoxiang

# Миграция всего проекта
yaoxiang-migrate --old-to-new --recursive src/

# Предпросмотр миграции (без изменения файлов)
yaoxiang-migrate --old-to-new --dry-run src/main.yaoxiang

Правила миграции:

yaoxiang
# Старый синтаксис
add(Int, Int) -> Int = (a, b) => { a + b }
main() -> Int = { println("Hello"); 0 }
main() = { println("Hello") }

# === Новый синтаксис: полная форма (сигнатура полная + заголовок lambda полный) ===
add: (a: Int, b: Int) -> Int = (a, b) => a + b
main: () -> Void = () => { println("Hello") }

# === Сокращение: опускаем заголовок lambda ===
add: (a: Int, b: Int) -> Int = a + b
main: () -> Void = { println("Hello") }

# === Сокращение: вывод HM ===
add = (a: Int, b: Int) => a + b              # выводится как (a: Int, b: Int) -> Int
main: () -> Void = { println("Hello") }                  # выводится как () -> Void

# === Простейшая форма ===
main: () -> Void = {                                      # эквивалентно main: () -> Void = { ... }
    println("Hello")
}

Зависимости ​

  • Нет внешних зависимостей
  • Может быть реализован независимо

Риски ​

РискВлияниеМеры по смягчению
Пропуск миграцииСтарый код не компилируетсяПредоставляется инструмент миграции, покрывающий все шаблоны старого синтаксиса
Ошибки парсераНестабильный синтаксический разборДостаточное покрытие тестами

Открытые вопросы ​

Следующие вопросы были решены в процессе проектирования и записаны в Приложении A.

  • Q1: Должна ли сохраняться предельно краткая запись main() = body? → Решено: Сохраняется как main: () -> Void = { ... }
  • Q2: Сохраняется ли : после имени функции? → Решено: Сохраняется опционально; но функции с параметрами по-прежнему требуют указания типов параметров в сигнатуре или в заголовке lambda
  • Q3: Поддерживает ли алгоритм HM вывод типов параметров? → Решено: Возвращаемый тип/локальные переменные могут быть выведены; типы параметров функций с параметрами должны быть указаны явно
  • Q4: Вводить ли ключевое слово fn? → Решено: Не вводится, функция — это lambda
  • Q5: Какова стратегия миграции старого кода? → Решено: Предоставляется инструмент yaoxiang-migrate
  • Q6: Как использовать generic-функции? → Решено: Используется унифицированный синтаксис RFC-010 (T: Type)

Приложения ​

Приложение A: Справка по синтаксису определения функций в разных языках ​

ЯзыкСтиль синтаксисаОсобенности
Rustfn add(a: i32, b: i32) -> i32 { ... }Ключевое слово + аннотация типа
Haskelladd a b = ... / add :: Int -> Int -> IntРазделение сигнатуры типа
OCamllet add a b = ...Типы параметров могут быть опущены
MoonBitfn add(a: Int, b: Int): Int { ... }Краткая аннотация типа
TypeScriptconst add = (a: number, b: number): number => ...Стиль lambda
Scaladef add(a: Int, b: Int): Int = { ... }Ключевое слово def
YaoXiangname = (a: Int, b: Int) => a + bФункция = lambda, HM выводит возвращаемый тип

Приложение B: Записи решений по дизайну ​

РешениеРешениеДатаЗаписано
Стиль синтаксисаНовый синтаксис name: (params) -> Return = body + вывод HM2026-02-03@沫郁酱
Положение параметровИмена параметров объявляются в сигнатуре, согласовано с RFC-0102026-02-03@沫郁酱
Заполнение по умолчаниюФункции без параметров могут опускать сигнатуру, пустой блок {} выводится как Void2026-02-03@沫郁酱
Вывод типовАлгоритм HM автоматически выводит типы, явные аннотации при невозможности вывода2026-01-06@沫郁酱
Старый синтаксисВыводится из употребления, предоставляются инструменты миграции2026-01-06@沫郁酱
Ключевое слово fnНе вводится2026-01-06@沫郁酱
Рекурсивное объявлениеАлгоритм HM и ограничения рекурсии автоматически выводят типы2026-01-06@沫郁酱

Приложение C: Глоссарий ​

ТерминОпределение
Алгоритм HMАлгоритм вывода типов Хиндли-Милнера, автоматически выводит типы функций и переменных
GenericsИспользование параметров типа (T: Type) для ограничения полиморфных функций, например identity: (T: Type) -> ((x: T) -> T) = x (RFC-010)
Заполнение типа по умолчаниюДля функций без параметров и без возврата опускается -> Void, компилятор заполняет автоматически
Синтаксический сахарУпрощённая запись синтаксиса, облегчающая чтение кода
НормализацияПреобразование синтаксических форм в унифицированное внутреннее представление
Функция — это lambdaПо своей сути функция — это lambda-переменная, тип выводится алгоритмом HM

Ссылки ​