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

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

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

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

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

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

Три операции

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

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

Уточнение

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

type module service {
component metrics { command Emit } // inherited
}
service Orders {
// Refining metrics — same kind, merges:
component metrics {
command EmitStructured // adds; Emit is still inherited
}
}

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

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

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

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

Переопределение

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

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

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

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

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

override — это смена вида с чистого листа

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

type module service {
component metrics { command Emit; command 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'.
command Read
command Write
}
}

Избыточный override

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

Отбрасывание

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

type module service {
component metrics { command Emit }
required cascade team
}
service TeamlessReports {
drop team // no 'team' 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
command CollectBatch // adds a new one
}
}

drop абсолютен на каскаде

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

drop применяется только к унаследованному

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

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

Инварианты

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

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

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

type module service {
component metrics { command Emit }
}
service Bad {
override component log { command Write } // ❌ 'log' was never inherited
drop antlers // ❌ 'antlers' was never inherited
required component logs { command Send } // ❌ 'required' + content
}

На уровне типа

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

type module service {
required cascade team
required database PrimaryStore
component metrics { command Emit }
}
// Subtype refining a required blank to a narrower kind:
type service transactionalService {
required relational_db PrimaryStore // refine to subtype kind, keep blank
}
// Subtype overriding to a non-subtype kind:
type service cacheBackedService {
override required cache PrimaryStore // override, keep blank
}
// Subtype fulfilling a requirement:
type service paymentsService {
team: "Payments" // fulfills 'required cascade team'
database PrimaryStore { command Charge } // fulfills required section
}
// Subtype dropping a default sub-declaration:
type service slimService {
drop metrics
}
// Subtype dropping the whole declaration:
type service teamlessService {
drop team
}

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

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

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

Одна операция на цель на тело

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

service Bad {
team: A
team: B // ❌ two assignments to 'team'
drop metrics
component metrics { ... } // ❌ drop + add on the same target
}

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

Проработанный дифф на обоих уровнях

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

Базовый тип:

type module service {
required cascade team
required database PrimaryStore
component metrics { command Emit }
}

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

type service readReplica {
required relational_db PrimaryStore // refine: narrower kind, keep blank
override component metrics_v2 { // ❌ 'metrics_v2' not inherited;
// override requires existing entity
command EmitV2
}
}

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

type service readReplica {
required relational_db PrimaryStore
component metrics_v2 { // ✅ fresh add, no override keyword
command EmitV2
}
}

Экземпляр readReplica:

readReplica AnalyticsDB {
team: Analytics
relational_db PrimaryStore { // fulfill (refining kind from inherited)
command Read
}
component metrics {
command EmitStructured // refine: adds, keeps inherited Emit
}
drop metrics_v2 // remove inherited
}

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

  • team: Analytics (заполнен каскадный пропуск).
  • relational_db PrimaryStore { command Read } (заполнен раздел с уточнённым подтипом).
  • component metrics { command Emit; command EmitStructured } (уточнено унаследованное).
  • Нет metrics_v2 (отброшено).

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

Резюме

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

Что дальше

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