Отвергнутые альтернативы

Идеи, которые рассматривались, но не приняты в дизайн Nova. Каждая альтернатива — с причиной отказа и ссылкой на D-решение, где было принято финальное правило.

Зачем сохранять. При следующих обсуждениях не возвращаться к уже отвергнутым идеям, или вернуться с новыми аргументами — зная, почему отказались раньше.

По темам


Парадигма и типы

Классы и наследование (Java/C#/Python)

Отвергнуто. Fragile base class, diamond problem, божественные классы. См. D1.

trait + impl Trait for Type (Rust)

Отвергнуто. Заменено структурным protocol (D42). Структурно — любой тип со совпадающими методами автоматически удовлетворяет, без impl-блоков.

Bounds на дженериках (fn f[T: Bound](x T))

Отвергнуто в MVP. Дженерики без bound’ов; требования описываются анонимным структурным типом параметра. См. D15.

dyn Trait vs impl Trait различение (Rust)

Отвергнуто. Усложняет язык, не нужно для прикладного.

Ассоциированные типы и default-методы протоколов

Отвергнуто в MVP. Может быть пересмотрено. См. D42.

Trait-наследование, specialization, HKT

Отвергнуто. Research-уровень, не нужно для прикладного.

Sum-type c = и запятой (старый D17 синтаксис)

Отвергнуто (D52 пересмотр). Раньше: type Color = Red, Green, Blue. Теперь: type Color | Red | Green | Blue — leading | обязателен, = для sum-type не используется. Sum-type c | (Haskell-style) был изначально отвергнут как «дубль запятой при наличии =», но после снятия = (D52) leading | стал основной формой. См. D52, evolution.md.

type X = { поля } со знаком равенства для record

Отвергнуто. = означает «справа выражение типа», для record формы данных не нужен. См. D17, history/evolution.md.

type X = { методы } для протоколов

Отвергнуто. Заменено keyword protocol (D42). Визуально путалось с record.

Per-field mut/final модификаторы

Отвергнуто. Слишком многословно (~18 mut в большом record). Заменено на «mut по умолчанию + readonly». См. D36.

mut self как параметр метода

Отвергнуто. Заменено на mut @method модификатор перед @. См. D35.

type X = Y для alias и sum-type (старый D17)

Отвергнуто (D52 пересмотр). D17 фиксировал «= справа выражение типа», но это спотыкалось на sum-type (список конструкторов — не выражение). D52 ввёл единую систему: type X Y — newtype, type X alias Y — alias, type X | A | B | C — sum через leading |, type X { ... } — record. = в декларациях типов вообще не используется. См. D52.

enum keyword как kind-токен для sum-type

Отвергнуто (рассмотрено в процессе D52). type Color enum { Red, Green, Blue } — длиннее leading | и не даёт дополнительной информации. Прецеденты: Rust/Scala/Swift enum. Выбран минимальный синтаксис type Color | Red | Green | Blue. См. D52.

Single-variant sum-type

Отвергнуто. type X | Only(int) запрещено компилятором — это дубль с record/newtype без выигрыша. Используется type X { value int } (record) или type X int (newtype). См. D52.

Конфликт discriminants в sum-варианте

Отвергнуто. type X | A = 5 | B = 5 — ошибка компиляции (B имеет то же значение, что A). Программист пишет уникальные discriminants. См. D52.

Implicit cast int → Sum через as

Отвергнуто. Type-небезопасно (число может не попасть в варианты). Только через pattern match с Fail[InvalidVariant]:

let c = match n {
    0 => Red
    1 => Green
    _ => throw InvalidColor
}

См. D52, D54.

protocol Foo { ... } как отдельный keyword (старый D42)

Отвергнуто (D53 пересмотр). Создавало асимметрию: protocol Foo объявлялся отдельным keyword’ом, но Foo использовался в позиции типа параметра. Программист спрашивал «если protocol — тип, почему не объявляется через type?». D53 сделал protocol kind-токеном: type Foo protocol { ... }. Прецедент Go (type X interface { }). См. D53.

Анонимный protocol без префикса (fn f(x { method() }))

Отвергнуто. Двусмысленно с record-литералами и блок-выражениями. С D53 анонимный protocol-тип записывается с префиксом: fn f(x protocol { method() }). Симметрично []T, (A, B), fn() -> T.

type any alias protocol { } для top-type

Отвергнуто. Для protocol’ов alias-форма семантически тождественна newtype-форме (структурная типизация делает имена незначимыми). Дополнительный синтаксис без выигрыша. Используется прямая форма type any protocol { }. См. D53.

Implicit shared scope для generic-параметров protocol’а

Отвергнуто. Идея: «если в одном protocol две сигнатуры с одинаковым [T], это один и тот же T; для другого типа — другая буква». Снижает локальность (нужно прочитать весь protocol-блок), невозможно выразить независимый T в нескольких методах без смены convention. В Nova явное разделение: protocol P[T] (Модель A, T на protocol-уровне) и method[T]() (Модель B, T на методе). См. D42, discussion-log этап 20.

dyn как имя top-type

Отвергнуто. Семантическая коллизия: в Rust dyn Trait — синтаксис trait object’а с runtime-dispatch. Программист с Rust-фоном ожидает trait object, а в Nova это просто пустой protocol-тип. См. D53, discussion-log этап 24.

Dynamic как имя top-type

Отвергнуто. В C# dynamic означает late binding (любые методы, проверка в runtime). У Nova не late binding, а runtime-tag для type-check. Семантически близко, но не идентично — программист с C#- фоном путается. Длиннее any. См. D53.

Variant как имя top-type

Отвергнуто. В C++ std::variant<T1, T2, ...>closed sum. В Nova у нас уже есть closed sum (type X | A | B), а Variant претендует на top-type. Терминологический конфликт. См. D53, discussion-log этап 24.

Anonymous unions (TS-style string | number)

Отвергнуто (зафиксировано как открытый Q-anonymous-union). Требует subtyping (string <: string | number), которого в Nova нет. Runtime-tag boxing на каждой границе — серьёзная стоимость. D55 даёт похожую эргономику через literal coercion в named sum’ы (type StrOrInt | S(str) | I(int), let x StrOrInt = "test") без subtyping.

Избыточная форма { name: name } и { field: @field }

Отвергнуто (D52). Когда имя поля record-литерала совпадает с источником-переменной или self-полем, программист обязан использовать shorthand. Обе формы — { key, value } и { key: key, value: value } — раньше были эквивалентны (D17 field punning). Теперь избыточная форма — compile error. То же для @field-доступа: { @end } обязателен вместо { end: @end }. Прецедент Rust/TS имеет lint, но не язык; Nova идёт строже — один способ для одного случая (D40/D43-стиль). См. D52.

Vec[T] как именованная обёртка над []T

Отвергнуто. Vec как type Vec[T] alias []T не давал выгоды — Vec[int][]int, имя только добавляло когнитивную нагрузку. Конструкторы Vec.new()/with_capacity() дублировали [] и []T.with_capacity(...). Решение — везде []T как единая форма; методы расширения определяются напрямую на []T через D35 (fn []T @map, @filter, etc.). examples/stdlib_vec.nv — методы расширения, не обёртка. См. D58, D52 (alias не используется для Vec).

Default-имя поля при embed (use Type без alias, Go-style)

Отвергнуто (D39 revised). Раньше D39 разрешал use Account без имени — поле получало имя Account (PascalCase). Это нарушало D30 (поля snake_case, типы PascalCase) и создавало визуальную несогласованность в record-блоке: audit_log рядом с Account. Auto-conversion HashMaphashmap/hash_map — magic, не очевидное правило. Решение: use name Type с обязательным alias. См. D39.

Subtyping для embed/delegation

Отвергнуто. use Parent — это композиция, не наследование. AuditedAccount не подтип Account. См. D39.


Эффекты

Lowercase эффекты (throws io async)

Отвергнуто. Эффекты — обычные типы, PascalCase. См. D2, D11.

!throws io async (с восклицательным знаком)

Отвергнуто.

  • ! похож на not (логическое отрицание), сбивает чтение.
  • Только перед первым эффектом — непоследовательно с несколькими.

См. D3.

<throws, io, async> (Koka-style в угловых скобках)

Отвергнуто. Угловые скобки нужны для дженериков, тяжелее читается.

~throws io async (тильда)

Отвергнуто. Неудобно набирать на части клавиатур, визуально слабее слов.

Атрибуты @throws @io @async

Отвергнуто. Лишние символы, @ ассоциируется с метаданными.

Эффекты выводятся молча, без видимости в публичном API

Отвергнуто. Эффекты должны быть видны на глаз в публичной сигнатуре. См. D28.

effect X { ... } keyword

Отвергнуто. Заменено на обычный type X { ... }. Эффект — частный случай типа. См. D18.

handler X = ... keyword

Отвергнуто. Handler — это значение, не отдельная категория. Использует record-литерал или handler-лямбду. См. D11, D31.

Fail без параметра E в публичных функциях

Частично отвергнуто. Бесконтекстный Fail уровня «может бросить что-то» не информативен в публичных API. Но D65 ввёл FailFail[any] для quick-and-dirty / catch-all сценариев. В публичных API параметр обязателен (рекомендация, не compile error). См. D25, D28, D65.

try { ... } catch (e) { ... } блок

Отвергнуто. Используется handler через with Fail[E] = ... { ... } — тот же механизм, что для всех эффектов.

try_panic/catch panic в коде

Отвергнуто. Panic перехватывается только runtime’ом на границе fiber’а. См. D13.


Память

Opt-in cycle collection (~T, ~&T, ~weak)

Отвергнуто. Заменено managed concurrent GC по умолчанию. См. D6, отменённый D21.

&T / mut &T borrow (Rust-style)

Отвергнуто. Не нужно при managed GC. Передача объекта = передача указателя. Slice уже эффективен. Lifetime checker — research-уровень.

Weak[T] тип в stdlib

Отвергнуто. Use cases решаются иначе — Cache[K, V] с TTL/LRU, handler-механизм для observer pattern.

Эффект Alloc[Cycle]

Отвергнуто. Аллокации в managed heap не отдельный эффект. См. D6.

Cycle collector Bacon-Rajan

Отвергнуто. Заменён на единый concurrent GC.

Compile-time анализ циклов через ~T

Отвергнуто. Не нужен — GC справляется.

Realtime: всегда явный region { ... } блок

Отвергнуто. Implicit region для тела Realtime-функции — компилятор оборачивает сам, дублирование контракта не нужно.


Синтаксис

: в type annotations (x: int, let y: u64 = 1)

Отвергнуто. Где компилятор однозначно знает «дальше тип» — двоеточие опускается. См. D17, spec/syntax.md.

Match-arms через -> (Rust-стиль)

Отвергнуто. -> уже занят для возвращаемого типа. Match arms — через =>. См. D19.

Тело функции через = (OCaml/F#-стиль)

Отвергнуто. = ассоциируется с присваиванием. Тело — через =>. См. D20, D22.

Дженерики через <T> (Rust/TS/Java-стиль)

Отвергнуто. Парсер-неоднозначность (sort<int>(xs) vs (sort < int) > (xs)). Используется [T]. См. D16.

Type suffixes 100u32, 1.5f32

Отвергнуто. Шум, дублирует inference. Используется as-cast (100 as u32). См. D44.

Implicit it в лямбдах (Kotlin)

Отвергнуто. Нелокальное поведение. Параметр всегда именован. См. D43.

spawn() { body } — spawn с пустыми скобками

Отвергнуто. spawn — keyword, не функция, поэтому правило D43 (trailing-block требует ()) к нему не применяется. Пустые () вводят в заблуждение: создают иллюзию вызова функции. Валидные формы: spawn expr и spawn { body }. См. D43.

Custom-операторы (:+, <>, >>= Scala-стиль)

Отвергнуто. Известный источник нечитаемого кода. Только фиксированный набор. См. D46.

&& / || для overloading

Отвергнуто. Short-circuit семантика должна быть строгой и предсказуемой. См. D46.

cond && throw X как guard-pattern

Отвергнуто. Перегрузка && (bool / control flow / side-effect) — анти-паттерн. Используется validate()? или if cond { throw X }.

Indentation-significant блоки (Python-стиль)

Отвергнуто. {} обязательны для блоков. => принимает только одно выражение. См. D40.

Обязательный ; в конце statement (C/Rust-стиль)

Отвергнуто. Newline разделяет statement’ы; ; опционален. См. D49.

Обязательный return в block-body

Отвергнуто. Последнее выражение блока — значение функции (Rust/Scala-стиль). return — для раннего выхода. См. D23.

ret вместо return

Отвергнуто. Отступление от 12 мейнстрим-языков без веской причины. Экономия 3 символов не оправдывает.

Void

Отвергнуто. Тип «без значения» — () (unit). См. D20.

Один оператор as/as?/as! (Swift-стиль)

Отвергнуто. Усложняет mental model — три формы одного оператора с разными свойствами (force-cast, optional-cast, throws-cast). В Nova разделение явное: as — compile-time конвертация, is — runtime type-check, try_as[T]() / as[T]? — методы для extract’а через Option/Throws. См. D54.

is T для произвольных типов (Kotlin-style)

Отвергнуто. Требует runtime-tag на всех значениях — стоимость во всём runtime. В Nova is ограничен any-значениями: tag только у них, локализованная стоимость. Sum-варианты проверяются через exhaustive match (compile-time). См. D54.

is для sum-вариантов (if x is Circle)

Отвергнуто. Дублирует match без выигрыша. Match даёт exhaustiveness check, if x is Circle — нет. Один механизм для одной задачи. См. D54.

Implicit cast между обычными типами без as

Отвергнуто. Все конвертации между типами явные через as. Implicit conversion введён только для literal coercion в позиции с явным типом (D55) — узкий механизм для sum-обёрток и record-литералов, не общая фича.

Tuple-coercion для multi-parameter конструкторов sum’а

Отвергнуто (отложено в MVP). Идея: let e Event = (5, 10)Click(5, 10). Двусмысленность с tuple-литералами как самостоятельными значениями. Сложно различать «tuple как значение» vs «tuple-coercion в multi-param». Не критично для use-case’ов. См. D55.

Coercion на цепочках конверсий (int → UserId → Wrapper)

Отвергнуто. D55 делает только одну обёртку вокруг exact-type значения. Цепочки усложняют правила и легко дают неожиданный результат. Программист пишет промежуточный cast явно (42 as UserId). См. D55.

Coercion без явной аннотации типа

Отвергнуто. let x = "test" остаётся str, не превращается в StrOrInt. Type inference не «угадывает» sum или record. Только явный target type активирует coercion. AI-locality сохраняется. См. D55.

Opt-in coercion через protocol (Swift ExpressibleByStringLiteral)

Отвергнуто. Программист объявляет sum/record, поведение coercion работает автоматически без дополнительного opt-in. Это менее гибко, но проще — не нужно реализовывать марker-протоколы для каждого типа. См. D55.

Record-coercion для sum-вариантов с record-формой

Отвергнуто. let s Shape = { radius: 5.0 } — нельзя выбрать между Circle и Square по совпадению полей. Программист обязан писать имя варианта (Circle { radius: 5.0 }). Type-driven parsing по структуре полей — антипаттерн в Nova. См. D55.

Default-значения параметров (fn f(x int = 10))

Отвергнуто. Прецеденты — Python, Kotlin, Swift, C++. Привлекательно эргономикой, но ломает несколько принципов Nova:

  1. «Всё видно в сигнатуре». Дефолт не виден на call-site — читатель не знает, какое значение использовалось. Анти-AI-friendly.
  2. «Один очевидный путь» (D9). f(x) или f(x, default_y()) — оба валидны, выбор путает.
  3. Refactoring fragility. Менять дефолт = тихое изменение поведения у всех вызывающих, кто «забыл» передать.
  4. Конфликт с overloading (D46). Резолюция перегрузки усложняется при default-значениях.
  5. Конфликт с контрактами (D24). requires/ensures зависят от значений — SMT-кодировка усложняется.

Альтернатива: опции-record + spread (D52 + D55 + D60):

type ServerOpts { port int, host str, max_conn int }
const SERVER_DEFAULTS ServerOpts = { port: 8080, host: "0.0.0.0", max_conn: 1024 }

fn serve(opts ServerOpts) Net -> ()

serve({ ...SERVER_DEFAULTS })                       // все дефолты
serve({ ...SERVER_DEFAULTS, port: 9000 })            // override

Все опции видны на call-site, дефолты переиспользуются между средами, refactoring безопасен. См. syntax.md → «Опциональные параметры».

Если когда-то вернёмся — только в named-form вызова и только для public API, отдельное D-решение.


Видимость и модули

pub (Rust-style)

Отвергнуто. Заменено на export для симметрии с import. См. D5.

Видимость по регистру (Go: Capital = public)

Отвергнуто. Мешает естественному именованию. См. D5.

public/private/protected/package (Java)

Отвергнуто. Overkill, никто не помнит правила. Два уровня достаточно. См. D5, D47.

private keyword

Отвергнуто. Конфликтует с философией Nova «явно публичное». Default — приватный, явный — export. См. D47.

Per-field export для record

Отвергнуто в MVP. Все поля публичны, convention _prefix для приватных. Может быть пересмотрено в будущем. См. D47.

Wildcard import import x.*

Отвергнуто. Скрывает источник, ломает локальность контекста. См. D29.

Циклические импорты

Отвергнуто. Compile error. Решается выделением в третий модуль. См. D29.

Несколько модулей в одном файле

Отвергнуто. Один файл = один модуль. См. D29.

#include или препроцессор

Отвергнуто. Только import через семантику модулей.

Static-поля и static-переменные

Отвергнуто. Глобальное mutable state — через handler. См. D41.

Companion-object (Kotlin)

Отвергнуто. То же что static, просто в обёртке. См. D41.

Lazy static (Rust lazy_static!)

Отвергнуто. Скрытое глобальное состояние с инициализацией. Если нужна ленивость — handler с lazy полем.


Operator overloading и литералы

Operator overloading через protocol/trait

Отвергнуто. Через имена методов (@plus, @eq) — структурно, без impl Add for T. См. D46.

Tagged template через макросы (Rust sql!("..."))

Отвергнуто. Tagged template — обычная функция с особым синтаксисом вызова. См. D48.

Префикс через символ (s"...", r"..." Scala-стиль)

Отвергнуто. Ограничивает имя одним символом, не позволяет user-defined теги. См. D48.

Implicit tag для строки (без префикса)

Отвергнуто. Создаёт ambiguity со строковыми литералами. Tag всегда явный. См. D48.

??= null-coalescing assignment

Отвергнуто. Десахар a ??= ea = a ?? e в JS/TS работает, потому что T | null — один тип, и ?? возвращает T | null (когда RHS — тоже T | null). В Nova a имеет фиксированный тип binding’а Option[T], а ?? (D67) разворачивает Option в T:

let mut a Option[int] = None
let b int = a ?? 5      // ✅ b: int

a ??= 5                  // ❌ десахар: a = a ?? 5 → LHS Option[int], RHS int

Альтернативная семантика «set-if-None» (a ??= e ≡ if a is None { a = Some(e) }) работала бы, но это не симметрично с прочими compound- ops (+=, -=, …): обычные compound — a OP= e ≡ a = a OP e, а гипотетический ??= стал бы исключением с собственной semantics. Лишний keyword/синтаксис ради одного редкого паттерна, который и так читаемо пишется через if let:

if let None = a { a = Some(5) }
// или:
a = a.or(Some(5))

См. D67 (?? оператор), D49 (compound-assign).


Concurrency

async / await keywords (Rust/JS-стиль)

Отвергнуто. Async — обычный эффект, без Future<T> в типе и без await в коде. Цвет функции исчезает. См. D14.

Future<T> / Promise<T> в типе возврата

Отвергнуто. Тип остаётся T, эффект Async в сигнатуре.

Stackless coroutines (Rust state machines)

Отвергнуто. Экономят память, но требуют Pin/Send/Sync бойлерплейта; не подходят для AI-кодинга.

OS threads as default

Отвергнуто. Слишком тяжёлые для миллионов задач.

await / маркер на месте вызова async-функции

Отвергнуто. Эффект Async в сигнатуре — единственная декларация. await-маркер дублирует информацию из сигнатуры и не даёт реальных гарантий после введения preemption (v1.0+). Прецедент Go/Erlang/ Java virtual threads. См. D14, D50.

Fire-and-forget spawn свободно (Go-style)

Отвергнуто. spawn разрешён только внутри structured-scope (supervised [+ опц. cancel:], parallel for, select; и stdlib race/with_timeout). Долгоживущие задачи — через detach { ... } с эффектом Detach в сигнатуре. Утечки fiber’ов как в Go-style fire-and-forget устраняются. См. D50.

Авто-детект блокирующих syscall’ов (Go runtime / Loom)

Отвергнуто. Сложно реализовать (deep runtime hooks), хрупко на нестандартных C-библиотеках, прячет важное поведение от сигнатуры. Nova берёт явную модель Tokio — blocking { ... } примитив с эффектом Blocking в сигнатуре. См. D50.

Раздельные эффекты Async и Par

Отвергнуто. Искусственное разделение, AI-unfriendly. parallel for — синтаксический примитив с эффектом Async. Декларация «эта функция fan-out» — через имя или док, не отдельный эффект. См. D50.

Custom Promise как магия компилятора

Отвергнуто. Promise[T] если нужен — пишется обычным кодом через handler-обёртку.


Контракты

«Всегда статическая проверка, без runtime-fallback»

Отвергнуто. Сделало бы контракты обязательно тотальными, половину прикладного кода невозможно было бы аннотировать. См. D24.

«Только runtime-проверка»

Отвергнуто. Теряется ценность контрактов в release. Превращает контракты в обычные assert.

Фиксация конкретного SMT-движка в дизайне

Отвергнуто. Дизайн фиксирует класс движка, имя — выбор реализации.


Spread в литералах

..arr (две точки) для spread в массиве/record

Отвергнуто. Конфликт с partial-pattern (Cons(..), [head, ..], D59) и range-литералом (a..b, D58). Используется ...arr (три точки). См. D60.

*arr / **obj для spread (Python-стиль)

Отвергнуто. * уже занят умножением; визуальный шум; ** для record не имеет аналогии в Nova. Используется ...arr (унификация для массива и record). См. D60.

OCaml with-syntax: { user with name: "bob" }

Отвергнуто. Keyword with уже занят под effect-binding (D11). Путаница неизбежна. Используется { ...user, name: "bob" }. См. D60.

Многократный spread в record-литерале

Отложено. В MVP — один spread на record, всегда первый. Многократный spread требует решения про конфликт ключей (last-wins / first-wins / ошибка). Открытый вопрос Q-record-spread-merge. См. D60.

Spread в pattern-position: match xs { [1, ...rest, 5] => ... }

Отложено. D59 уже ввёл ..rest для slice-bind в array-pattern. Spread в pattern (с ...) — отдельный вопрос; решается, когда понадобится. См. D60.


Тело функции, лямбды, handler-method

=> { block } для лямбд (Kotlin/JS-стиль (x) => { ... })

Отвергнуто. Лямбда строго (params) => expr (одно выражение). Делало бы лямбды «маленькими функциями с императивом», стирало бы границу между «значение-выражение» и «именованная функция». Если нужен блок с let’ами — использовать named fn. См. D22.

=> { block } для тел fn-функций

Отвергнуто. Общий закон: => и {} не сочетаются. Если нужен блок — fn name(...) { ... } без =>. См. D40.

=> { block } для handler-method

Отвергнуто. Симметрия с fn: handler-method имеет две формы — op(p) => expr или op(p) { block } (без =>). См. 04-effects.md → D31.

Guard-цепочки в =>-теле fn

Отвергнуто. Раньше D23 показывал fn classify(x) -> str => if x < 0 { return "n" } ... "big" — цепочка statement’ов после =>. Это противоречило D40=> = ровно одно выражение»). Сейчас guard’ы только в блок-форме fn name(...) { ... }. См. D23.

Trailing-lambda как «лямбда»

Отвергнуто как формулировка. Грамматически f(args) { stmts; expr } — это trailing-block, не лямбда. Лямбда строго (params) => expr (D22); trailing-block может содержать let’ы и несколько statement’ов — он семантически блок, не значение-функция. Синтаксис не изменился, изменилась классификация. См. D43.

Match-arm без => (pattern { block })

Отвергнуто. Без => теряется единый маркер «начало результата»; парсер хуже различает arm с блоком от arm с guarded-pattern и от вложенного блока в сложном pattern’е. Match-arm — единственное исключение из правила «=> и {} не сочетаются»: разрешена форма pattern => { block }. См. D19.


Эффекты

resume как keyword (Koka-стандарт)

Отвергнуто. resume(value) — литературное имя для возобновления континуации в handler-методе. Точно описывает алгоритм, но требует от пользователя понимания концепции continuation’ов. В семантике D61 tail-only handler-method ведёт себя как обычная функция — возвращаемое значение идёт в caller операции, программа продолжается. Точнее это описывается обычным return v / финальным выражением; resume уходит как термин, который путает больше чем помогает. См. D61.

Полная continuation-семантика (multi-step resume, Koka/OCaml 5)

Отложено. В full-semantics handler-method может выполнить код до и после возврата continuation. Это требует stack-снимков (corosensei в OCaml 5) или CPS-преобразования (Koka). В backend-нише Nova почти не нужно: реальные handler’ы (Throws, Db, Logger, Time, Random) укладываются в tail-position resume. Сложность реализации не оправдана. Если когда-нибудь потребуется — отдельный D-блок расширит семантику обратно совместимо. См. D61 «Эволюция», Q-multishot-resume.

Multi-shot resume

Отложено как Q-multishot-resume. Backend Nova не нуждается в backtracking-эффектах (Prolog-стиль, недетерминизм). Если потребуется для compute-tasks — отдельный keyword-маркер вроде multishot resume ради сохранения «один resume = один путь по умолчанию». См. D61.

effect Db { ops } для handler-литерала (двойное использование effect)

Отвергнуто. effect уже занят под объявление типа эффекта (type Db effect { ... }). Использование того же keyword’а для handler-литерала создаёт двусмысленность «что именно объявляется — тип или значение?». Раздельные keyword’ы effect (тип) и handler (значение) явно различают роли. Тип значения в системе типов — Handler[E], что согласовано с keyword’ом handler. См. D61.

Effect[E] или Impl[E] как имя типа handler-значения

Отвергнуто. Handler[E] — стандартное имя из литературы (Eff, Koka, Effekt). Effect[E] тавтологично — E уже эффект. Impl[E] не передаёт смысл «реализация именно эффекта». Сохраняем литературный термин ради совместимости с академическими источниками. См. D61.

Implicit interrupt для Never-операций без keyword’а

Отвергнуто. Идея: финальное выражение handler-method’а Never-операции автоматически считается interrupt (потому что return для Never запрещён). Семантика финального выражения зависела бы от типа операции: для обычной операции это «значение для caller’а» (return), для Never — «значение для всего with-блока» (interrupt). AI-unfriendly, программист должен помнить контекст. Явный interrupt v для Never-handler’ов однозначен. См. D61.

Двукратное использование protocol для эффектов и структурных интерфейсов

Отвергнуто (revised D53). Раньше D53 объединил эффекты и structural-protocol’ы под одним keyword’ом protocol. Опыт показал — это разные семантические контракты:

  • protocol — статический dispatch на типе значения
  • effect — lookup активного handler’а в with-стеке

В D53-эпоху код типа fn f[T: Db](x T) парсился, но семантически ломался (Db нельзя использовать как обычный protocol-bound). D61 расщепляет: protocol остаётся для интерфейсов значений, effect — отдельный keyword для эффектов. Смешение compile-time запрещено.