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 задаёт значение напрямую.
Мотивация
Зачем нужна эта возможность?
- Согласованность синтаксиса: Устранение исторического наследия старого синтаксиса, унификация стиля
- Краткость: Алгоритм HM автоматически выводит типы, сокращая шаблонный код
- Безопасность типов: Алгоритм HM гарантирует безопасность типов, явные аннотации только когда вывод невозможен
- Зрелость языка: Алгоритм HM — зрелое решение современных функциональных языков
Модель унифицированного синтаксиса
Ключевой принцип: name: Signature = LambdaBody
- Полная форма: Сигнатура (включая имена параметров + типы +
->+ тип возврата) + Заголовок lambda (включая имена параметров) - Правила сокращения: Опускать, когда это не вносит неоднозначности
->не может быть опущен (признак function type, иначе будет разобрано как tuple)- При наличии входных параметров типы параметров должны быть явно указаны как минимум в одном из мест — в сигнатуре или в заголовке lambda
- Заголовок lambda может быть опущен → если сигнатура уже объявляет имена и типы параметров
- Тип возврата может быть указан явно или опущен, когда он может быть выведен
# Полная форма (сигнатура полная + заголовок 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Цели дизайна
# === Полная форма ===
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 полные |
| Опускаем заголовок lambda | name: (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 не поддерживаются:
# Правильно: используем фигурные скобки
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 через параметры-генерики, реализация композиции полиморфных функций
Примеры:
# ✅ Поддерживается полиморфизм высшего ранга: генерик ограничивает параметр типа функции
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:
- Идентифицировать параметры функций высшего порядка:
f: (T) -> T - Создать ограничение-генерик:
(T: Type) - Вывести конкретные типы через инстанциацию генериков
- Реализовать композицию полиморфных функций
Правила синтаксиса lambda-выражений
Важное правило: Значение блока кода { ... } определяется хвостовым выражением (единственный выход); return — нелокальный выход типа Never, выходящий из ближайшей границы функции. Форма выражения = expr задаёт значение напрямую. Подробности см. в RFC-010a.
| Форма синтаксиса | Синтаксис | Выход значения |
|---|---|---|
| Форма блока | { statements } | Хвостовое выражение (пустой блок {} — Void) |
| Форма выражения | expression | Значение выражения |
return | return e | Нелокальный выход из функции, тип Never |
Примеры:
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") # Правильно: форма выраженияОсновные идеи:
- Определение функций использует алгоритм HM для вывода типов — максимальный вывод, явная ошибка при невозможности вывода
- Принцип работы алгоритма HM: Автоматический вывод типов через контекстную информацию, такую как ограничения типов операторов и отношения вызовов функций
- Поддержка generics: Полиморфные функции используют синтаксис generics
(T: Type)для явного ограничения параметров типа (RFC-010/011) - Границы вывода: Тип возврата и локальные переменные могут быть выведены; типы параметров функций с параметрами должны быть указаны явно (в сигнатуре или в заголовке lambda)
- Функции без параметров и без возврата используют
name: () -> Void = { ... }, согласовано с RFC-010 - Старый синтаксис выводится из употребления, предоставляются инструменты миграции
Примеры вывода типов:
# 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)) }# === Определение функций: вывод типов алгоритмом 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 полные |
| Опускаем заголовок lambda | name: (a: Type, b: Type) -> Ret = ... | Заголовок lambda | Сигнатура уже объявляет параметры |
| Опускаем сигнатуру | name = (a: Type, b: Type) => ... | Сигнатура | Заголовок lambda задаёт типы параметров |
| Опускаем тип возврата Ret | name: (a: Type, b: Type) -> = ... | Тип возврата | HM выводит тип возврата |
| Полная форма без параметров | name: () -> Void = () => { ... } | Нет | Полная функция без параметров |
| Сокращённая форма без параметров | name: () -> Void = { ... } | Заголовок lambda | Опускаем () => |
| Простейшая форма без параметров | name = { ... } | Всё | Простейшая форма без параметров и без возврата |
| Присваивание переменной | name = value | Тип | HM выводит тип |
| Явная переменная | name: Type = value | Нет | Явная аннотация типа |
Ключевые принципы:
->— признак function type, не может быть опущен (иначе будет разобрано как tuple)- Тип возврата
Retможет быть опущен, HM выводит его из тела функции - При наличии входных параметров типы параметров должны быть явно указаны (в сигнатуре или в заголовке lambda)
- Остальные части могут быть опущены, когда они могут быть выведены и это не вносит неоднозначности
- Никаких неявных преобразований типов, чтобы избежать хаоса в стиле JavaScript
Подробный дизайн
Раскрытие синтаксического сахара
Независимо от сокращений, всё в итоге нормализуется к унифицированному промежуточному представлению:
// Полная форма
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
};Определение синтаксиса
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 не требуетсяОбработка ошибок
# === Примеры ошибок компиляции ===
# Ошибка 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: Синтаксический разбор и алгоритм HM (v0.3)
- Реализовать новый синтаксис
name = lambda+ вывод типов алгоритмом HM - Реализовать заполнение по умолчанию для функций без параметров и без возврата
- Реализовать новый синтаксис
Фаза 2: Инструменты миграции (v0.3)
- Разработать инструмент
yaoxiang-migrate --old-to-new - Автоматическое преобразование кода со старым синтаксисом
- Разработать инструмент
Фаза 3: Валидация и документация (v0.3)
- Проверка завершённости миграции старого кода
- Обновление документации
Инструмент миграции
# Миграция одного файла
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Правила миграции:
# Старый синтаксис
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: Вводить ли ключевое слово→ Решено: Не вводится, функция — это lambdafn?Q5: Какова стратегия миграции старого кода?→ Решено: Предоставляется инструментyaoxiang-migrateQ6: Как использовать generic-функции?→ Решено: Используется унифицированный синтаксис RFC-010(T: Type)
Приложения
Приложение A: Справка по синтаксису определения функций в разных языках
| Язык | Стиль синтаксиса | Особенности |
|---|---|---|
| Rust | fn add(a: i32, b: i32) -> i32 { ... } | Ключевое слово + аннотация типа |
| Haskell | add a b = ... / add :: Int -> Int -> Int | Разделение сигнатуры типа |
| OCaml | let add a b = ... | Типы параметров могут быть опущены |
| MoonBit | fn add(a: Int, b: Int): Int { ... } | Краткая аннотация типа |
| TypeScript | const add = (a: number, b: number): number => ... | Стиль lambda |
| Scala | def add(a: Int, b: Int): Int = { ... } | Ключевое слово def |
| YaoXiang | name = (a: Int, b: Int) => a + b | Функция = lambda, HM выводит возвращаемый тип |
Приложение B: Записи решений по дизайну
| Решение | Решение | Дата | Записано |
|---|---|---|---|
| Стиль синтаксиса | Новый синтаксис name: (params) -> Return = body + вывод HM | 2026-02-03 | @沫郁酱 |
| Положение параметров | Имена параметров объявляются в сигнатуре, согласовано с RFC-010 | 2026-02-03 | @沫郁酱 |
| Заполнение по умолчанию | Функции без параметров могут опускать сигнатуру, пустой блок {} выводится как Void | 2026-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 |
