Перейти к содержимому

19. Уточнение, переопределение и отбрасывание

Раз типы штампуют содержимое на экземпляры и на подтипы, естественный следующий вопрос: как нижестоящие сущности редактируют то, что им дано?

Ответ ArchLang — три операции: уточнение, переопределение и отбрасывание, — которые применяются единообразно по двум осям:

  • На уровне типа: подтип изменяет то, что предоставляет его родительский тип.
  • На уровне экземпляра: экземпляр изменяет то, что предоставляет его тип.

В обоих случаях операции одни и те же. В этом единообразии и есть весь смысл этой главы.

ДействиеКлючевое словоЧто делает
Уточнить(нет)Переобъявить унаследованную сущность с тем же типом (или подтипом). Новое содержимое сливается с унаследованным.
ПереопределитьoverrideЗаменить унаследованную сущность типом, не являющимся подтипом исходного. Намеренно, требует ключевого слова.
ОтброситьdropПолностью удалить унаследованную сущность из этой области и ниже.

Это весь словарь. Три ключевых слова; обе оси.

Когда вы переобъявляете унаследованную сущность с тем же типом (или его подтипом), операция называется уточнением — ключевое слово не требуется. Новое содержимое сливается с унаследованным:

type module service {
component metrics { rest_create emit } // inherited
}
service Orders {
// Refining metrics — same type, merges:
component metrics {
rest_create emitStructured // adds; emit is still inherited
}
}

У Orders.metrics в итоге будут и Emit (унаследованное), и EmitStructured (добавленное). Уточнение — самая лёгкая операция; это то, к чему обращаются, когда хотят добавить к данному.

Уточнение также может сужать — переобъявить унаследованный обязательный пропуск с типом-подтипом, сохранив пропуск:

type module service {
required database PrimaryStore
}
type service transactional_service {
required relational_db PrimaryStore // relational_db extends database
// still blank, narrower type
}

То же семейство типов (relational_db — подтип database), поэтому ключевое слово не нужно.

Когда вы переобъявляете унаследованную сущность типом, который не является подтипом исходного, нужно ключевое слово override. Это намеренный сигнал: я заменяю это чем-то другим.

type module service {
component metrics { rest_create emit } // inherited
}
service Reports {
// Switching from 'component' to 'database' — not a subtype relationship.
// Without 'override', the validator rejects this.
override database metrics {
aspect { storage: "postgres" }
}
}

override может сохранить результат пустым, сочетаясь с required:

type module service {
required database PrimaryStore
}
service CacheOnly {
override required cache PrimaryStore // switch to cache type, stay blank
}

Почему ключевое слово обязательно. Уточнение должно читаться гладко — добавление команды к унаследованному компоненту не должно быть визуально шумным. Но молчаливая замена component на database скрыла бы то, что на самом деле происходит. Ключевое слово override вынуждает автора признать «здесь я делаю что-то отличное от уточнения», а ревьюеров — это увидеть. Аналогично необходимости unsafe в Rust — визуально громко там, где семантика необычно разрешительна.

Когда вы override к новому типу, родословная нового объявления — это шаблон нового типа, а не старого. Любой drop X.Y внутри override-нутого тела ссылается только на детей нового типа. Содержимое старого типа исчезло, не сливается.

type module service {
component metrics { rest_create emit; rest_create flush }
}
service Reports {
override database metrics {
// 'metrics' is now a database — no emit, no flush. The component
// template is not in scope here. Lineage starts from 'database'.
db_read read
db_write write
}
}

Использование override там, где достаточно уточнения (тот же тип или подтип), допустимо, но помечается как избыточное инструментарием. Та же идея, что и public на методах интерфейса в Java — ключевое слово сохраняет намерение; предупреждение предотвращает деградацию.

drop X полностью удаляет унаследованную сущность из этой области и потомков:

type module service {
component metrics { rest_create emit }
required cascade version
}
service VersionlessReports {
drop version // no 'version' field at all in this subtree
drop metrics // no 'metrics' component either
}

drop X.Y удаляет конкретного потомка:

service Reports {
component metrics {
drop emit // removes the inherited emit
rest_create collectBatch // adds a new one
}
}

drop не заслоняет; он разрывает каскадную цепочку в этой точке. Потомки не возобновляют чтение из более глубокого предка. Рассмотрено в главе 18.

Отбрасывание того, чего нет в области видимости, — ошибка валидации:

type module service {
component metrics { rest_create emit }
}
service Bad {
drop antlers // ❌ 'antlers' was never inherited; nothing to drop
}

Валидатор обеспечивает следующее:

  • override применяется только к унаследованным сущностям. Использование на свежем объявлении — ошибка.
  • drop применяется только к унаследованным сущностям. Отбрасывание того, чего нет в области видимости, — ошибка.
  • required и содержимое взаимно исключающи. required component logs { rest_create Send } — противоречие: required означает нет значения; фигурный блок означает вот значение. Валидатор это отвергает.

Три диагностики, по одной на каждый инвариант:

type module service {
component metrics { rest_create emit }
}
service Bad {
override component log { rest_create write } // ❌ 'log' was never inherited
drop antlers // ❌ 'antlers' was never inherited
required component logs { rest_create send } // ❌ 'required' + content
}

Те же три операции, применяемые подтипом к родительскому типу:

type module service {
required cascade version
required database PrimaryStore
component metrics { rest_create emit }
}
// Subtype refining a required blank to a narrower type:
type service transactional_service {
required relational_db PrimaryStore // refine to subtype type, keep blank
}
// Subtype overriding to a non-subtype type:
type service cache_backed_service {
override required cache PrimaryStore // override, keep blank
}
// Subtype fulfilling a requirement:
type service payments_service {
version: "2.1" // fulfills 'required cascade version'
database PrimaryStore { db_write charge } // fulfills required section
}
// Subtype dropping a default sub-declaration:
type service slim_service {
drop metrics
}
// Subtype dropping the whole declaration:
type service versionless_service {
drop version
}

Подтип, как и экземпляр, может применять эти операции только к сущностям, которые он унаследовал от родительского типа. Подтипы, конечно, могут также добавлять свежие объявления своего собственного.

Подтипы не могут менять поведение каскада

Заголовок раздела «Подтипы не могут менять поведение каскада»

Одно ограничение на уровне типа: подтипы не могут переопределять поведение cascade/append. Режим распространения зафиксирован типом, который вводит поле. Если service объявляет cascade version, ни один подтип не может сделать version некаскадным. Это намеренно — это сохраняет ментальную модель «что делает распространение для этого поля?» устойчивой по всей цепочке типов.

Данная цель — поле, аспект или под-объявление — может быть адресована не более одного раза на тело. Две операции на одной цели в одном теле — ошибка валидации:

service Bad {
version: "1.0"
version: "2.0" // ❌ two assignments to 'version'
drop metrics
component metrics { ... } // ❌ drop + add on the same target
}

Если вам нужно «стереть и заменить», делайте это в двух областях (подтип, который отбрасывает, и экземпляр, который добавляет) или через override (который заменяет за один шаг).

Сценарий, сочетающий всё из этой главы.

Базовый тип:

type module service {
required cascade version
required database PrimaryStore
component metrics { rest_create emit }
}

Подтип уточняет, переопределяет и отбрасывает:

type service read_replica {
required relational_db PrimaryStore // refine: narrower type, keep blank
override component metrics_v2 { // ❌ 'metrics_v2' not inherited;
// override requires existing entity
rest_create emitV2
}
}

Второе объявление на самом деле — свежее добавление, override к нему неприменим. Исправление:

type service read_replica {
required relational_db PrimaryStore
component metrics_v2 { // ✅ fresh add, no override keyword
rest_create emitV2
}
}

Экземпляр read_replica:

read_replica AnalyticsDB {
version: "4.2"
relational_db PrimaryStore { // fulfill (refining type from inherited)
db_read read
}
component metrics {
rest_create emitStructured // refine: adds, keeps inherited emit
}
drop metrics_v2 // remove inherited
}

К чему приходит AnalyticsDB:

  • version: "4.2" (заполнен каскадный пропуск).
  • relational_db PrimaryStore { db_read read } (заполнен раздел с уточнённым подтипом).
  • component metrics { rest_create emit; rest_create emitStructured } (уточнено унаследованное).
  • Нет metrics_v2 (отброшено).

Каждая операция читается ясно; ничего не подразумевается неявно.

  • Три операции: уточнение (без ключевого слова), переопределение (требует ключевого слова), отбрасывание (требует ключевого слова).
  • Уточнение сливает новое содержимое с унаследованным; работает для переобъявлений того же типа или подтипа.
  • Переопределение — для смен типа, не являющихся уточнениями подтипа: намеренно, громко, с обязательным ключевым словом.
  • Отбрасывание полностью удаляет унаследованную сущность; каскадные цепочки разрываются на отбрасывании.
  • Три инварианта: override/drop применяются только к унаследованным сущностям; required и содержимое взаимно исключающи.
  • Подтипы могут использовать те же три операции по отношению к родительским типам; они не могут менять поведение каскада.
  • Одна операция на цель на тело.

Глава 20: Виджеты → — заключительная глава о том, как пользовательский рендер встраивается в модель.