RFC-007: Унифицированная схема синтаксиса определения функций
Резюме
Данный RFC определяет финальную схему синтаксиса определения функций в языке YaoXiang. Используется унифицированный синтаксис name: (params) -> Return = body, полностью соответствующий модели name: type = value из RFC-010.
Для избежания неоднозначностей: при наличии входных параметров функции типы параметров должны быть явно указаны либо в «сигнатуре», либо в «заголовке lambda» (минимум в одном месте); опущение типов с обеих сторон не допускается.
В блоках кода { ... } необходимо использовать return для возврата значения; при отсутствии return по умолчанию возвращается Void. Выражение вида = expr непосредственно возвращает значение.
Мотивация
Зачем нужна эта функция?
- Синтаксическая согласованность: устранение исторического багажа старого синтаксиса, унификация стиля
- Краткость: алгоритм HM автоматически выводит типы, сокращая шаблонный код
- Типовая безопасность: алгоритм HM гарантирует типобезопасность, явное указание требуется только когда вывод невозможен
- Зрелость языка: алгоритм HM — это проверенное решение современных функциональных языков
Унифицированная модель синтаксиса
Основной принцип: name: Signature = LambdaBody
- Полная форма: сигнатура (с именами параметров + типы +
->+ тип возврата) + заголовок lambda (с именами параметров) - Правила сокращения: опущение допускается везде, где это не приводит к неоднозначности
->нельзя опускать (маркер функционального типа, иначе будет распознано как кортеж)- При наличии входных параметров типы параметров должны быть явно указаны либо в сигнатуре, либо в заголовке 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 = { println("Hello") } # минимальная форма (выводится как () -> Void)
# === Обобщённые функции (с использованием унифицированного синтаксиса 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 для возврата значения; при отсутствии return по умолчанию возвращается Void. Выражение вида = expr непосредственно возвращает значение.
Внимание: -> является маркером функционального типа и не может быть опущен (иначе будет распознано как кортеж).
Важно: выражение 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) через параметризованные типы.
Принцип проектирования:
- Функции высшего порядка: при передаче функций в качестве параметров необходимы generics-ограничения для типов функций
- Форма записи типовых аннотаций:
(T: Type) -> ((f: (T) -> T, x: T) -> T)- параметризованные ограничения для типов функций - Рабочий процесс HM: вывод типов функций через параметризованные экземпляры, реализация полиморфной композиции функций
Пояснение примеров:
# ✅ Поддержка полиморфизма высших порядков: generics-ограничения для параметров типа функций
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
# ❌ Не поддерживается: функции высшего порядка без generics-ограничений
# bad_hof: (f, x) => f(f(x)) # HM не может вывести, отсутствуют параметризованные параметрыПроцесс вывода HM:
- Идентификация параметров функций высшего порядка:
f: (T) -> T - Создание generics-ограничений:
(T: Type) - Вывод конкретных типов через параметризованные экземпляры
- Реализация полиморфной композиции функций
Синтаксические правила для Lambda-выражений
Важное правило: в блоках кода { ... } необходимо использовать return для возврата значения; при отсутствии return по умолчанию возвращается Void. Выражение вида = expr непосредственно возвращает значение.
| Синтаксическая форма | Синтаксис | Способ возврата |
|---|---|---|
| Блок кода | { statements } | Обязательно использовать return; при отсутствии return по умолчанию Void |
| Выражение | expression | Непосредственный возврат значения выражения |
Примеры:
main: () -> Void = { println("Hello") } # Возвращает Void (нет return)
add: (a: Int, b: Int) -> Int = { return a + b } # Возвращает Int (явный return)
empty: () -> Void = {} # Пустой блок по умолчанию возвращает Void
# Ранний возврат: использование return
factorial: (n: Int) -> Int = {
if n <= 1 { return 1 }
return n * factorial(n - 1)
}
# Форма выражения: непосредственный возврат значения (без return)
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 - Старый синтаксис выводится из эксплуатации, предоставляются инструменты миграции
Примеры вывода типов:
# Обобщённые функции: явные параметры типа (использование унифицированного синтаксиса 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
}
# Полиморфные функции: определение через явные generics-ограничения (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
# Полиморфизм высших порядков: реализация поддержки 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 = { 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
# Обобщённые функции: явное ограничение параметров полиморфного типа (использование унифицированного синтаксиса 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 | Нет | Явная типовая аннотация |
Основные принципы:
->является маркером функционального типа и не может быть опущен (иначе будет распознано как кортеж)- Тип возврата
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 # Функциональный тип (имена параметров в сигнатуре)
| type_expr '->' type_expr # Простой функциональный тип
| identifier '(' type_expr (',' type_expr)* ')' # Применение типа
expression ::= '(' parameters ')' '=>' block
| '(' ')' '=>' block
| '(' parameters ')' '=>' expression
parameters ::= parameter (',' parameter)*
parameter ::= identifier # Вывод типа
| identifier ':' type_expr # Частично явный тип
block ::= '{' statement (',' statement)* '}'
| expression
statement ::= identifier ':' expression # Оператор присваивания
| expression # Оператор-выражение (выполняется, но не возвращает)
| 'return' expression # Оператор return (возвращает указанное значение)
# Внимание: в блоках кода необходимо использовать return для возврата значения; при отсутствии return по умолчанию возвращается Void
# Например: { return 1 + 1 } возвращает Int; { println("Hello") } возвращает Void
# Внимание: параметры generics используют синтаксис (T: 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 не объявлен; требуются явные generics-параметры (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 | Использование алгоритма Hindley-Milner | ✅ Принято, стандарт современных функциональных языков |
| Явные типы | Все типы должны быть явноно написаны | Противоречит принципу упрощения синтаксиса, увеличивает шаблонный код |
| Сохранение старого синтаксиса | Одновременная поддержка старого и нового синтаксиса | Раскол синтаксиса, высокая стоимость поддержки |
| Ключевое слово fn | Введение fn для различения функций и переменных | Противоречит принципу «функция — это lambda» |
Стратегия реализации
Фазы разделения
Phase 1: Синтаксический анализ и алгоритм HM (v0.3)
- Реализация нового синтаксиса
name = lambda+ вывод типов алгоритмом HM - Реализация заполнения по умолчанию для функций без параметров и возврата
- Реализация нового синтаксиса
Phase 2: Инструмент миграции (v0.3)
- Разработка инструмента
yaoxiang-migrate --old-to-new - Автоматическая конвертация кода со старым синтаксисом
- Разработка инструмента
Phase 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 = { println("Hello") } # Выводится как () -> Void
# === Минимальная форма ===
main = { # Эквивалентно main: () -> Void = { ... }
println("Hello")
}Зависимости
- Нет внешних зависимостей
- Может быть реализовано независимо
Риски
| Риск | Влияние | Меры снижения |
|---|---|---|
| Пропуск миграции | Ошибка компиляции старого кода | Предоставление инструмента миграции, покрывающего все паттерны старого синтаксиса |
| Ошибки парсера | Нестабильность синтаксического анализа | Достаточное тестовое покрытие |
Открытые вопросы
Следующие вопросы решены в процессе проектирования, записаны в Приложении A.
Q1: Следует ли сохранить форму записи→ Решено: сохранено какmain() = bodyв качестве предельно краткой?main = { ... }Q2: Сохраняется ли→ Решено: опционально; но для функций с параметрами типы параметров должны быть указаны в сигнатуре или заголовке lambda:после имени функции?Q3: Поддерживает ли алгоритм HM вывод типов параметров?→ Решено: возвращаемое значение/локальные переменные могут быть выведены; типы параметров функций с параметрами требуют явного указанияQ4: Вводится ли ключевое слово→ Решено: не вводится, функция — это lambdafn?Q5: Какова стратегия миграции старого кода?→ Решено: предоставляется инструментyaoxiang-migrateQ6: Как использовать обобщённые функции?→ Решено: использование унифицированного синтаксиса 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 | Алгоритм вывода типов Hindley-Milner, автоматически выводит типы функций и переменных |
| Generics | Использование параметров типа (T: Type) для ограничения полиморфных функций, например identity: (T: Type) -> ((x: T) -> T) = x (RFC-010) |
| Заполнение типа по умолчанию | Функции без параметров и возврата опускают -> Void, компилятор автоматически заполняет |
| Синтаксический сахар | Синтаксическое упрощение для улучшения читаемости кода |
| Нормализация | Преобразование синтаксических форм в унифицированное внутреннее представление |
| Функция — это lambda | Функция по сути является lambda-переменной, тип автоматически выводится алгоритмом HM |
