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

7. Процессы

Процесс — это упорядоченная, возможно ветвлёная последовательность вызовов интерфейсов. Он отражает бизнес- и технические потоки: оформление заказа, обработчик вебхука, ежедневное пакетное задание, последовательность онбординга пользователя.

Процессы — основной источник информации о зависимостях в ArchLang. Стрелки между модулями на ваших диаграммах выводятся из шагов процессов. Удалите процесс — и стрелки, которые он подразумевал, исчезнут. Отдельной операции «нарисовать зависимость» не существует.

process Checkout {
Customer > Orders.createOrder
Orders > Inventory.reserve
Orders > Payments.authorize
Payments > Ledger.record
Orders > Notifications.sendEmail
}

Пять шагов; шесть задействованных модулей; пять выведённых стрелок зависимостей (Customer → Orders, Orders → Inventory, Orders → Payments, Payments → Ledger, Orders → Notifications).

Модули — это актёрский состав; процессы — это история, а история и есть то, чем бизнес на самом деле является. Процесс, можно сказать, важнее модуля из этой пары. Моделируйте их столько, сколько сможете; в идеале — моделируйте все.

Покрытие — это то, что доказывает, что ваши модули не мертвы. Модуль, который не появляется ни в одном процессе, по определению ни с чем не взаимодействует — ни с чем не связан. Это и есть форма мёртвого кода.

Правило. Модуль, не участвующий ни в одном процессе, читается как мёртвый код. Почти всегда это означает, что не хватает процесса, а не что модуль простаивает, — найдите поток, в котором он участвует, и смоделируйте его.

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

Каждый шаг имеет форму:

Caller > Callee.Interface
  • Caller — модуль или актёр (сущность, выполняющая вызов). Слева от >. Внутри тела модуля его можно опустить — тогда он по умолчанию равен объемлющему модулю (см. Анонимность: минимальный черновик).
  • Callee — в зрелой форме интерфейс (обработчик, который запускается). Справа от >.

Стрелка > читается как «вызывает». Является ли нижележащий транспорт синхронным, асинхронным или каким-то ещё, определяется типом интерфейса на принимающей стороне. С базовым типом interface язык трактует каждый вызов единообразно; типы из стандартной библиотеки (command, event, …, Глава 11) добавляют различия sync/async и стилизацию рёбер. Событие — не более чем вызываемая сторона асинхронного типа, достигаемая шагом >; нет ни subscribes:, ни отдельной обвязки событий.

Черновик, а не ошибка. Поместить модуль с правой стороны — Payments > Orders — разрешено. Резолвер синтезирует анонимный интерфейс на Orders и отслеживает его как TODO. Это минимальный черновик (ниже); завершённая модель называет интерфейс, и ворота слияния с нулём TODO это обеспечивают.

Процессы могут жить в трёх местах, все семантически эквивалентные:

Внутри тела модуля. Принадлежит этому модулю; полностью квалифицированное имя становится Owner.Process:

module Orders {
process Checkout {
User > Orders.ordersResource.add
}
}

Верхний уровень с in. Объявляется плоско, прикрепляется к телу модуля. Зеркалит модульный синтаксис in:

in Orders process Fulfillment {
Orders > Shipping.schedule
}

Форма in позволяет каждой команде объявлять процессы для модуля другой команды, не редактируя файл этой команды.

Верхний уровень. Отдельно стоящая оркестрация, не принадлежащая ни одному модулю:

process CheckoutFlow {
Customer > Orders.createOrder
Orders > Payments.authorize
}

Размещение семантически эквивалентно, но не произвольно — объявляйте процесс в наименьшей области видимости, которая содержит охватываемый им пролёт:

  • Целиком внутри одного сервиса → локальный процесс на этом сервисе.
  • Охватывает несколько сервисов, и у вас есть системный модуль, который их содержит → объявите его на системе.
  • Охватывает несколько сервисов в архитектуре с одной системой без зонтичного модуля → процесс верхнего уровня (корневой) — это нормально. Корень — законный владелец.

Правило. Процесс живёт там, где находится владелец охватываемого им пролёта. Не поднимайте локальный для сервиса поток к корню и не выдумывайте системный модуль только ради того, чтобы разместить процесс.

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

Ссылайтесь на модули и интерфейсы, которых ещё не существует. Customer > Orders.createOrder, когда нет ни модуля Orders, ни интерфейса createOrder, — это не ошибка: компилятор синтезирует их как заглушки (рисуются пунктиром) и записывает каждый пробел как TODO — «недостающая деталь», а не «неправильно» (Спецификация §4.13, §10). Ваш поток рисуется немедленно; модули выпадают из него.

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

Правило. Внутри пакета неопределённая локальная ссылка — это заглушка TODO, а не ошибка. (Межпакетная ссылка устроена иначе — пакеты непрозрачны, поэтому неопределённый внешний тип остаётся жёсткой ошибкой.)

Это зеркальное отражение модуль-вперёд, двухпроходного потока: модуль-вперёд подходит для документирования системы, которую вы уже знаете; процессы-вперёд — для наброска новой или предлагаемой. Оба сходятся к одной и той же модели.

Черновик процессы-вперёд делает неопределённые ссылки TODO. Тот же принцип покрывает анонимные: всё анонимное, неуказанное или неопределённое — это TODO компилятора — единый класс чернового долга. «Завершённость» означает: ничего анонимного и ничего неопределённого, — а ворота слияния (archlang validate --complete, гейт приёма в Studio) держат эту черту. Это позволяет легальному черновику быть максимально лёгким, не пуская его при этом в завершённую модель.

Голые шаги — без обёртки process. Шаг, записанный в корне файла или прямо в теле модуля, и есть объявление — это сахар для анонимного процесса из одного шага:

module Orders {
> Payments // анонимный процесс, неявный caller = Orders
> Cart.add // назовите интерфейс; caller всё ещё неявный
}

Внутри тела модуля caller можно опустить — он по умолчанию равен объемлющему модулю, поэтому > Payments.charge означает Orders > Payments.charge. В корне файла объемлющего модуля нет, так что caller остаётся явным.

Правило. Каждый голый шаг — это свой собственный анонимный процесс, и голые шаги не подразумевают порядка между собой. Два голых шага — это два независимых ребра, а не последовательность. Порядок и группировка приходят только из явной обёртки process { … } (пусть даже анонимной). Анонимный процесс сам по себе — это TODO; продвинуть его означает дать ему имя.

Завершающая . — неуказанный интерфейс. Завершающая точка помечает терминальный интерфейс как анонимный — TODO, рисуется пунктиром:

Auth > Billing.charge // зрело: интерфейс charge на Billing
Auth > Billing. // модуль Billing, интерфейс не указан (TODO)
Auth > Billing.Ledger. // подмодуль Ledger, интерфейс не указан (TODO)

Путь самотипизируется по позиции: внутренний сегмент (у которого дальше есть дети) — это модуль; терминальный сегмент — это интерфейс (объявление всегда побеждает эту догадку). Завершающая . нужна только для того, чтобы заставить иначе-неоднозначный терминал быть модулем с анонимным интерфейсом; > Payments и > Payments. означают одно и то же, поэтому каноническая форма убирает точку, и форматтер срезает лишнюю, как только объявление подтверждает модульность.

Цепочки — A > B > C. Цепочка — это встроенная, упорядоченная форма process { } для прямолинейного прогона:

Storefront > Orders.create > Payments.charge
// разворачивается в: Storefront > Orders.create ; Orders > Payments.charge

Переносимый вперёд caller каждого перехода — это верхний модуль вызываемой стороны предыдущего перехода (сервис — это актёр; вложенная деталь остаётся инкапсулированной). Цепочка — это один упорядоченный анонимный процесс; неявный caller применяется только к голове; привязка (x = A > B > C) захватывает последний переход. Цепочки только линейны — веер не цепляется (две вызываемые стороны из одного модуля — это два шага) — и предназначены для коротких, стабильных прогонов.

Ворота несущие. Всё вышесказанное позволяет черновику воспроизвести ровно тот веер из коробок-и-стрелок, от которого язык и существует, чтобы уйти (модуль с десятью строками > X). Что делает это безопасным: каждая такая стрелка — это анонимный процесс плюс анонимный интерфейс — все TODO, — поэтому завершённая модель физически не может их удержать. Допустимо на входе, запрещено на выходе.

Прежде чем перейти к конструкциям потока управления — одна идея, которая пронизывает их все: у каждого действия есть владелец. Узел — это [владелец] <действие>, тот модуль или актор, который его выполняет. Владение единообразно по вызовам и по потоку управления, поэтому на вопрос «кто это делает / кто решает эту ветвь / кто ведёт эту сагу» всегда есть ответ.

Владелец шага — это его вызывающая сторона (слева от >). Узлы потока управления берут префикс-владельца так же: Orders if …, Booking each 3 try …, Orders parallel join …. В остальном владелец разрешается по фиксированной цепочке:

явный префикс → | (веер: голова предыдущего соседа) → ближайший объемлющий владелец → TODO
\ (змейка: хвост предыдущего соседа) — разрешается здесь и дальше не проваливается
  • | веер повторяет голову предыдущего соседа — субъект, открывший ту строку, так что после цепочки это голова цепочки, а не промежуточный переход. Плоский способ сгруппировать прогон по одному исполнителю:

    process Fulfill {
    Orders > Inventory.reserve
    | > Payments.charge // владелец: Orders (веер)
    | if "vip": Orders > Notifications.send
    }
  • \ змейка — зеркальный глиф: он берёт хвост предыдущего соседа (верхний модуль его последнего перехода) и продолжает прогон оттуда, так что каждый участник назван один раз, а эстафета движется дальше.

    process Refund {
    Customer > CustomerPortal.webUI
    \ > APIGateway.publicAPI // владелец: CustomerPortal (змейка)
    \ > Orders.getOrder // владелец: APIGateway (змейка)
    \ > Payments.refund // владелец: Orders (змейка)
    | > Notifications.sendEmail // владелец: Orders (веер от змейки)
    }

    Мнемоника. | остаётся на голове, \ переходит на хвост.

    Прогон-змейка — это та же цепочка, что пишет A > B.x > C.y, только каждая строка остаётся отдельным шагом: у перехода свой комментарий, своя привязка as и своя идентичность в диффе. \ работает только с вызовами: пишите \ > Callee; глиф не может стоять перед note, блоком, управляющей конструкцией, do, go или действием unwind (parse.snakeRequiresCall). И он никогда не проваливается дальше: если хвост брать не у кого, шаг получает TODO-caller и находку SNAKE_NO_TAIL, а не наследует объемлющего владельца.

  • Блок-владелецOwner { … } — задаёт владельца для всего своего тела.

  • Унаследованный — владелец контейнера по умолчанию действует на его поддерево. Побеждает ближайший объемлющий владелец.

  • dist обозначает распределённое управление: нет единого координатора; структуру реализуют её участники (хореография). Это разрешённое состояние, а не TODOdist reversible { … } — это хореографированная сага. dist не распространяется: голый потомок внутри dist-пролёта наследует ближайшего не-dist владельца, иначе становится TODO.

Правило. Голое всегда означает наследовать, никогда не эмерджентно. Смысл узла никогда не зависит от того, где он стоит — эмерджентность — это всегда только явный dist. Если ничто не разрешает владельца, узел — это TODO.

do и go — без владельца: это структурные операции, а не действия, поэтому владельца они не несут (префикс на do — это шаблонный вызывающий-по-умолчанию, ниже).

Именно это и делает так, что оркестрация против хореографии вытекает из модели без флага-режима: оркестрованный поток опирается на одного унаследованного владельца (координатора); хореографированный перекладывает владение по шагам и помечается dist.

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

if — это исключающий выбор: Owner if cond Body (else if cond Body)* (else Body)?. Условие без тела — это черновая ветвь. Условие — идентификатор или строка в кавычках; тело — блок в фигурных скобках или одиночный шаг с префиксом :.

process Checkout {
Customer > Orders.createOrder
Orders > Payments.authorize
Payments if "stripe-customer" {
Payments > Stripe.charge
} else {
Payments > PayPal.createOrder
PayPal > PayPal.captureOrder
}
Payments > Ledger.record
}

select — это многопутевой выбор, и он инклюзивный: запускается каждый подходящий вариант:

process Checkout {
Orders select "channels" {
email: Orders > Notifications.email
sms: Orders > Notifications.sms // оба запустятся, если оба подходят
}
}

Owner select [count] [head] { case-label Body ... }. Никакого ключевого слова case — каждый блок начинается со своей метки (идентификатор или строка в кавычках), затем тело. Счётчик сужает, сколько вариантов срабатывает:

  • select (голый) — запускаются все подходящие варианты.
  • select oneпервый подходящий вариант (one = 1).
  • select Nпервые N подходящих, по порядку.
  • parallel select { … } — подходящие варианты запускаются конкурентно (см. Параллельность).

Граф зависимостей включает каждый интерфейс, упомянутый в любом из вариантов; головы и метки — свободные аннотации (рендерятся, не вычисляются).

Owner each <head> { ... } итерирует. Голова свободна: each item in xs, each "until ready" или голый счётчик each 3. Используется для веерного распространения по множеству неизвестной мощности.

process NotifyAll {
Notifications each subscriber in NotificationList {
Notifications > subscriber.sendUpdate
}
}

Повтор — это форма each <bound> try / catch / else: попытка, восстановление между попытками, повтор и сдача по исчерпании предела:

process WriteWithRetry {
Orders each 3 try {
snapshot = Orders > Store.read(id)
Orders > Store.write(id, update, snapshot)
} catch "conflict" {
Orders "перезагрузить + переприменить" // восстановление перед следующей попыткой
} else {
fail "write conflict" // 3 попытки провалились
}
}

При сбое запускается catch, и цикл повторяется; при успехе он выходит; когда предел исчерпан без успеха, запускается else. try, вложенный в обычное тело each (each: try { … }), — это, наоборот, пер-итерационный страж: успех не приводит к выходу, и else нет.

Обычный Owner try Body (catch [Label] Body)* пробует шаг или блок; при сбое запускается подходящий catch, и поток продолжается вперёд (без повтора). Допускается несколько catch; метка у каждого необязательна.

process Checkout {
Customer > Orders.createOrder
Orders try {
Orders > Payments.authorize
} catch "declined" {
Orders > Notifications.sendEmail
}
}

Owner parallel { branch { … } branch: step } исполняет свои ветки конкурентно. Условие слияния — префикс перед блоком, оно решает, когда поток продолжается:

  • parallel join N { … } — продолжить, когда завершатся N веток, оставив остальные работать (N опущено = все → AND-слияние).
  • parallel race N { … } — продолжить, когда завершатся N, отменив остальные (N опущено = 1 → побеждает первый / отложенный выбор).
  • parallel out { … } — отсоединить; не ждать.
process OrderFulfillment {
Orders > Shipping.createShipment
Shipping parallel {
branch: Shipping > Notifications.sendEmail
branch: Shipping > Notifications.sendSms
}
Shipping > Orders.updateOrder
}

Каждая ветка — это branch: + однострочник, branch { … } или вложенный узел управления (branch if …). Ветки могут быть именованными, а слияние может быть булевым по именам веток (and / or и скобки, регистронезависимо — без not); формы со счётчиком — сахар для симметричных порогов:

process FraudCheck {
Orders parallel join (fraud and credit) or override {
branch fraud: Orders > Fraud.check
branch credit: Orders > Credit.check
branch override: Manager "ручное подтверждение"
}
}

Веерное распространение совмещает parallel с each — одно тело цикла, исполняемое конкурентно:

process FanOut {
Orders parallel join 3 each item in items {
Orders > Worker.process(item)
}
}

Входящий шаг — Them > Us.x, когда внешняя сторона вызывает нас, — это ожидание: поток продолжается, когда они позвонят. Отдельного примитива событий нет; входящее против исходящего — это просто направление стрелки.

process Pay {
Customer > Booking.payOnline // Booking ждёт, пока клиент заплатит
}

Owner await <free-form> — это отложенное по времени ожидание, принадлежащее ожидающему:

process HoldSeat {
Booking await 1 day
}

Ожидание-или-таймаут — это race входящего шага против await:

process PayOrTimeout {
Booking parallel race {
branch: Customer > Booking.payOnline // входящий: ждём клиента
branch {
Booking await 1 day
fail "payment timeout"
}
}
}

Блок reversible { … } — это прямой поток, чьи завершённые шаги откатываются, когда он не может завершиться. Триггер по умолчанию — on error (сбой внутри блока) и может быть указан явно (reversible on error { … }).

Прямой шаг спаривается с обратным действием через unwind. A > B.x unwind Y — это в точности try { A > B.x } catch: { Y; rethrow }: если шаг сбоит — собственным сбоем или сбоем, пришедшим позже, — запускается Y, и сбой продолжает распространяться, так что более ранние unwind тоже срабатывают, в обратном порядке.

process PlaceOrder {
Order reversible {
Order > Inventory.reserve unwind Order > Inventory.release // компенсируем резерв
Order > Payments.charge unwind Order > Payments.refund // компенсируем списание
Order > Shipping.ship unwind Order > Shipping.cancel // компенсируем отправку
}
}

unwind каждого шага — это его собственная компенсирующая транзакция, а не отмена предыдущего шага. Сбойный шаг сам так и не завершился, поэтому отменять у него нечего. Когда позже сбоит другой шаг, повторный выброс движка запускает unwind уже завершённых более ранних шагов в обратном порядке: при сбое shiprefund, затем release; при сбое charge — только release. Терминальному шагу-коммиту unwind не нужен: после него уже нечему сбоить, чтобы его вызвать.

Что нужно знать:

  • Y должен быть полным оператором — вызовом с разрешимым владельцем (unwind Order > Payments.refund), терминатором (unwind fail) или блоком (unwind { … }). Голый интерфейс без > (unwind refund) — это ошибка: вызывающая сторона неизвестна.
  • unwind может прикрепляться к шагу или к блоку ({ … } unwind Y); по одному на блок; блок без unwind при откате пропускается.
  • unwind не может вкладываться в управление (например, внутрь if). Оберните управление в блок и поместите unwind на блок.
  • Строка может быть только-откатнойunwind X без прямого действия.

Оркестрованное против хореографированного — это, опять же, просто паттерн владения. У владеемого reversible есть один координатор, ведущий и вперёд, и назад. dist reversible хореографирован: каждый участник владеет своим шагом и своим unwind, который отменяет его собственную работу, без координатора. Компенсация по-прежнему нацелена на ту же вызываемую сторону, что и прямой шаг (reserverelease) — никогда не на предыдущего участника:

process PlaceOrderSaga {
dist reversible on error {
Customer > Order.create unwind Customer > Order.cancel
Order > TicketInventory.reserve unwind Order > TicketInventory.release
TicketInventory > Payment.charge unwind TicketInventory > Payment.refund
Payment > Badge.issue // терминальная выдача — после неё нечего отменять
}
}

Компенсация живёт в процессе, никогда — на интерфейсе. Правильный откат зависит от контекста потока — одна и та же операция компенсируется по-разному в разных сагах, — а обратная маршрутизация — это структура процесса.

> Identity.verify as checkpoint именует шаг. go <name> снова входит в этот именованный шаг и возобновляет движение вперёд от него — структурированный возврат-в-цикл. Свободной стрелки на метку нет; go всегда нацелен на реальное именованное действие.

process Onboarding {
Identity > Identity.verify as checkpoint
Identity "ручная проверка"
go checkpoint // возобновить с шага-чекпойнта (go без владельца)
}
  • fail "…" завершает ветку неудачей — и внутри reversible запускает откат.
  • finish "…" завершает ветку успехом.

Опускайте очевидное — но только если оно инкапсулировано

Заголовок раздела «Опускайте очевидное — но только если оно инкапсулировано»

Процесс — это история, а хорошая история пропускает тривиальное. Микросервис, сохраняющий в свою собственную инкапсулированную базу данных, не нуждается в шаге «сервис сохраняет в БД» — относитесь к сервису как к актору и позвольте вложенной базе быть той очевидной деталью, которую он несёт на себе. БД всё равно заслуживает места в модели (она документирует структуру); просто ей необязательно появляться в процессе.

Критерий — инкапсуляция, а не «кажется очевидным». Вы можете опустить вещь, только когда она целиком вложена в актора, которого вы упоминаете. Никогда не опускайте высокоуровневого соседа:

  • ✅ Опустите собственную вложенную базу сервиса — обращайтесь к сервису.
  • ❌ Не опускайте шлюз. «Всё идёт через шлюз» создаёт ощущение, что его можно пропустить, но шлюз — высокоуровневый сосед, а не инкапсулированная деталь, и именно в нём живёт политика. Пропуск его в процессе может скрыть нарушение политики.

То же правило инкапсуляции включает быстрые высокоуровневые черновики: можно написать процесс, где крупные системы общаются друг с другом, не называя сервисы внутри них. Это остаётся черновиком — более низкоуровневое описание обычно правдивее — и даже в черновике не выбрасывайте шлюз.

Повторяющаяся последовательность шагов становится подпроцессом — переиспользуемым помощником, вызываемым через do:

subprocess RecordEvent(name) {
Orders > Ledger.record
}
process Checkout {
Customer > Orders.createOrder
do RecordEvent(checkout_started)
Orders > Payments.authorize
do RecordEvent(payment_authorized)
}

Четыре вещи, которые надо знать про do:

  • Аргументы — только для выражения намерения. Это голые идентификаторы — не проверяются по типам, не связываются и не потребляются движком. Передача checkout_started документирует «этому подпроцессу, вероятно, нужно вот это» и делает место вызова читаемым — не более.
  • Прямые ссылки допустимы. Вы можете do подпроцесс, которого ещё не существует; пробел — это заглушка TODO, а не ошибка — то же правило неполноты-по-замыслу, что и при черновике процессов-вперёд. Определите его позже.
  • do — без владельца. Он вклеивает фрагмент в поток, а не выполняет работу. Необязательный префикс на do — это не владелец, а шаблонный вызывающий-по-умолчанию (следующий пункт).
  • Экспорт подпроцесса делает его точкой входа между пространствами. export-нутый подпроцесс — санкционированный способ для другого пространства вызвать поведение, не видя его внутренних шагов; сочетайте его со шлюзом для контролируемых межпространственных вызовов (Глава 12).

Переиспользование подпроцесса от другого исполнителя — on Caller. Подпроцесс может объявить параметр-вызывающего, чтобы один и тот же фрагмент читался от разного исполнителя в каждом месте вставки. Префикс на do связывает его:

subprocess chargePayment on Caller {
Caller try: Caller > Payment.charge
catch: fail "payment failed"
try: Payment > Invoice.issue
catch: Invoice > Support.newTask; finish
}
process SellTicket {
TicketInventory do chargePayment // Caller = TicketInventory
}

Caller явно упоминается в шагах подпроцесса и одновременно служит владельцем-по-умолчанию для его шагов с опущенным владельцем. Необязательный префикс в месте do (TicketInventory do chargePayment) связывает его.

Подпроцессы могут быть верхнего уровня (видны везде), объявленными внутри тела модуля (ограничены процессами этого модуля) или объявленными внутри тела типа (отштамповываются на каждый экземпляр — см. Глава 18).

Порядок поиска при выполнении do X(...):

  1. Подпроцессы, объявленные лексически внутри вызывающего процесса.
  2. Подпроцессы на владеющем модуле (побеждает ближайший предок).
  3. Отдельно стоящие подпроцессы верхнего уровня.

Процессы несут стабильные идентификаторы так же, как и модули:

process #t9p2mx Checkout {
Customer > Orders.createOrder
}

Форматтер выдаёт #t9p2mx при сохранении. Идентификатор позволяет диффам распознавать переименованный процесс. См. Главу 13.

Валидатор обеспечивает форму процесса, оставляя неполноту в покое:

  • Вызывающий (слева от >) разрешается в модуль или актёр (включая stdlib-модули user после импорта стандартной библиотеки — см. Главу 11) либо в имя, привязанное ранее в процессе, — или же, внутри тела модуля, опускается и по умолчанию равен объемлющему модулю.
  • Вызываемый (справа от >) разрешается в интерфейс, поверхность (которая маршрутизирует к своим интерфейсам) или модуль. Вызываемый-модуль — не ошибка: он синтезирует анонимный интерфейс, отслеживаемый как TODO (минимальный черновик выше).
  • Имя, используемое в несовместимых ролях в разных шагах — как листовой интерфейс в одном месте и как модуль-с-детьми в другом, — является ошибкой. Это не может быть истинным одновременно.

Что не является ошибкой: шаг, называющий локальный модуль, интерфейс или подпроцесс, которого ещё не существует, или оставляющий интерфейс анонимным. Внутри пакета такая ссылка синтезирует заглушку и отслеживается как TODO (см. Черновик процессов-вперёд) — недостающая деталь, а не противоречие. Межпакетные ссылки — исключение: пакет непрозрачен, поэтому неопределённая внешняя ссылка остаётся жёсткой ошибкой.

Ошибки появляются как диагностики LSP в вашем редакторе и как результаты с кодом 1 от archlang validate; TODO всплывают как черновой долг, который можно посчитать, а не как сбои.

Когда у вас есть процессы, автоматически следует несколько вещей:

  • Граф вызовов сервисов. Модули, связанные рёбрами Caller > Callee.Interface по всем вашим процессам.
  • Критические пути. Поток запроса в десять шагов виден с одного взгляда.
  • Радиус поражения. Когда один сервис падает, какие процессы ломаются? Тривиально выводится.
  • Влияние изменений. Когда вы удаляете интерфейс, валидатор сообщает вам про каждый шаг процесса, который от него зависел.

Ничто из этого не требует поддерживать отдельный каталог зависимостей. Каталог — это объединение каждого шага процесса в рабочем пространстве.

  • Процесс — последовательность шагов Caller > Callee.Interface.
  • Вызывающие — модули или акторы (или опущены в теле модуля — по умолчанию объемлющий модуль); вызываемые — листовые интерфейсы в зрелой форме.
  • У каждого действия есть владелец ([владелец] <действие>); владелец разрешается как явный → | веер (голова предыдущего соседа) → ближайший объемлющий → TODO (зеркальная \ змейка берёт хвост предыдущего соседа и не проваливается дальше: без ответа это TODO-caller плюс SNAKE_NO_TAIL), а dist обозначает эмерджентное/хореографированное управление.
  • Процессы несут на себе архитектуру — моделируйте их столько, сколько сможете; модуль, не участвующий ни в одном процессе, читается как мёртвый код. Детализация (if / select / parallel / each / try / await / reversible / as/go) — сахар; неглубокий процесс всё равно засчитывается.
  • Объявляйте каждый процесс в наименьшей области видимости, которая содержит его пролёт; корень — законный владелец в модели с одной системой.
  • Можно начинать с процессов-вперёд: неопределённые локальные модули и интерфейсы становятся пунктирными заглушками, отслеживаемыми как TODO, а не ошибками.
  • Минимальный черновик: голый шаг (без обёртки process), цепочка A > B > C, неуказанный интерфейс с завершающей . и вызываемый-модуль — все валидны; каждая анонимная часть — это TODO, а ворота слияния с нулём TODO держат их вне завершённой модели.
  • Опускайте только инкапсулированную деталь (собственную БД сервиса); никогда не опускайте высокоуровневого соседа вроде шлюза.
  • Подпроцессы — переиспользуемые помощники, вызываемые через do; их аргументы только выражают намерение, прямые ссылки допустимы, а экспортированный подпроцесс — точка входа между пространствами.
  • Граф зависимостей выводится из процессов; стрелки отдельно не рисуются.

Интерфейс может нести поле latency: 200ms (или диапазон, 50ms-500ms); анализатор сворачивает эти значения вдоль графа процесса, чтобы оценить критический путь, — он доступен для запроса как @estimatedLatency в проекциях и политиках. См. Главу 34: Анализ задержек.

Процесс рендерится не только как пошаговый обзор потока, показанный на протяжении этой главы, — тот же процесс может рендериться как диаграмма sequence или как коллаборация bpmn (flow <ProcessRef> sequence / flow <ProcessRef> bpmn). См. Главу 33: Представления процессов.

Глава 8: Проекции → — кураторские проекции модели для конкретных аудиторий.