Skip to content

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

Резюме

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

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

В блоках кода { ... } необходимо использовать return для возврата значения; при отсутствии return по умолчанию возвращается Void. Выражение вида = expr непосредственно возвращает значение.

Мотивация

Зачем нужна эта функция?

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

Унифицированная модель синтаксиса

Основной принцип: name: Signature = LambdaBody

  • Полная форма: сигнатура (с именами параметров + типы + -> + тип возврата) + заголовок lambda (с именами параметров)
  • Правила сокращения: опущение допускается везде, где это не приводит к неоднозначности
    • -> нельзя опускать (маркер функционального типа, иначе будет распознано как кортеж)
    • При наличии входных параметров типы параметров должны быть явно указаны либо в сигнатуре, либо в заголовке 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 = { 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
Опущен заголовок lambdaname: (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:

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) через параметризованные типы.

Принцип проектирования:

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

Пояснение примеров:

yaoxiang
# ✅ Поддержка полиморфизма высших порядков: 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:

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

Синтаксические правила для Lambda-выражений

Важное правило: в блоках кода { ... } необходимо использовать return для возврата значения; при отсутствии return по умолчанию возвращается Void. Выражение вида = expr непосредственно возвращает значение.

Синтаксическая формаСинтаксисСпособ возврата
Блок кода{ statements }Обязательно использовать return; при отсутствии return по умолчанию Void
ВыражениеexpressionНепосредственный возврат значения выражения

Примеры:

yaoxiang
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")               # Правильно: форма выражения

Ключевые идеи:

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

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

yaoxiang
# Обобщённые функции: явные параметры типа (использование унифицированного синтаксиса 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)) }
yaoxiang
# === Определение функций: вывод типов алгоритмом 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
Опущен заголовок 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НетЯвная типовая аннотация

Основные принципы:

  • -> является маркером функционального типа и не может быть опущен (иначе будет распознано как кортеж)
  • Тип возврата 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   # Функциональный тип (имена параметров в сигнатуре)
       | 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-правила

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

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 не объявлен; требуются явные 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»

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

Фазы разделения

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

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

    • Разработка инструмента yaoxiang-migrate --old-to-new
    • Автоматическая конвертация кода со старым синтаксисом
  3. Phase 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 = { println("Hello") }                  # Выводится как () -> Void

# === Минимальная форма ===
main = {                                      # Эквивалентно main: () -> Void = { ... }
    println("Hello")
}

Зависимости

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

Риски

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

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

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

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

Ссылки