Отвергнутые альтернативы
Идеи, которые рассматривались, но не приняты в дизайн Nova. Каждая альтернатива — с причиной отказа и ссылкой на D-решение, где было принято финальное правило.
Зачем сохранять. При следующих обсуждениях не возвращаться к уже отвергнутым идеям, или вернуться с новыми аргументами — зная, почему отказались раньше.
По темам
- Парадигма и типы
- Эффекты
- Память
- Синтаксис
- Видимость и модули
- Operator overloading и литералы
- Concurrency
- Контракты
Парадигма и типы
Классы и наследование (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
}
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 HashMap → hashmap/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
ввёл Fail ≡ Fail[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:
- «Всё видно в сигнатуре». Дефолт не виден на call-site — читатель не знает, какое значение использовалось. Анти-AI-friendly.
- «Один очевидный путь» (D9).
f(x)илиf(x, default_y())— оба валидны, выбор путает. - Refactoring fragility. Менять дефолт = тихое изменение поведения у всех вызывающих, кто «забыл» передать.
- Конфликт с overloading (D46). Резолюция перегрузки усложняется при default-значениях.
- Конфликт с контрактами (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 ??= e ≡ a = 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 запрещено.