Глоссарий GRAIN
Слова, которыми пользуется стандарт. Примеры — из разбора эндпоинта, чтобы термин был не определением из учебника, а строкой кода.
Чистая функция
Функция, результат которой зависит только от аргументов, и которая ничего не меняет вокруг себя. Вызови её дважды с теми же аргументами — получишь тот же ответ, и в мире ничего не изменится.
derivePublishedTrack(track, nowMs) // чистая: только считает новое значение
saveTrack(track) // не чистая: пишет в базуЧистые функции тестируются без подготовки: ни базы, ни сети, ни подмены времени. Поэтому у них в GRAIN своя роль файла — *.pure.ts.
Побочный эффект
Всё, что функция делает «наружу»: запись в базу, HTTP-запрос, лог, изменение глобальной переменной, чтение системных часов. Эффекты не запрещены — они собраны в предсказуемых местах, а не рассыпаны по коду.
Недетерминизм
Источник значений, который завтра даст другой ответ на тот же вопрос: Date.now(), Math.random(), crypto.randomUUID(), process.env. GRAIN держит их на границе, а внутрь домена они приходят аргументом — иначе поведение кода невозможно ни повторить, ни проверить.
Граница
Место, где код общается с внешним миром: HTTP-обработчик, gRPC-метод, консьюмер очереди, точка входа процесса, клиент чужого API. Роли entry, route, rpc, job, wire. На границе можно то, чего нельзя внутри: брать время, читать окружение, знать про фреймворк.
Домен
Смысловая часть программы: то, что осталось бы, если убрать HTTP, базу и фреймворк. Правила публикации трека — домен. Код статуса 403 — не домен.
Контракт
Обещание, которое функция даёт вызывающему, ещё до того как он прочитает её тело. В GRAIN контракт несёт имя: findTrack обещает, что вернёт null, если трека нет, и не бросит; getTrack обещает значение или исключение. Нарушать контракт нельзя — на него рассчитывают.
Инвариант
Условие, которое обязано быть истинным всегда, иначе программа сломана. «У опубликованного трека есть обложка» — инвариант. Проверяется через assert*: если он нарушен, это баг, а не ожидаемая ситуация.
Отказ и исключение
Отказ — ожидаемый исход, часть контракта: не найдено, нет прав, не хватает средств. Возвращается значением с кодом домен.предмет.причина.
Исключение — то, чего быть не должно: упала база, нарушен инвариант, ошибка в коде. Бросается и ловится только на границе.
Смешивать их — главный источник кода, где catch ловит всё подряд и превращает в «что-то пошло не так».
Идемпотентность
Свойство операции, при котором повторный вызов ничего не меняет. Публикация уже опубликованного трека должна оставить его опубликованным, а не сдвинуть дату. В GRAIN за это отвечает глагол ensure*, а в SQL — if not exists в миграциях.
Guard, ранний выход
Проверка в начале функции, которая отсекает невозможное и сразу выходит. Вместо лестницы if / else — плоский список условий:
if (track === null) return makeFail('music.track.missing', '…')
if (!canPublish(track, actor.id, actor.isAdmin)) return makeFail('music.track.forbidden', '…')Каждый guard читается отдельно, и ни один не требует держать в голове остальные.
Воронка
Форма функции в GRAIN: guard → acquire → derive → effect. Отсечь невозможное, добыть данные, посчитать, записать. Такты не перемешиваются; если тактов оказалось два комплекта — перед тобой две функции.
Роль файла
Одна ответственность, объявленная в имени: *.store.ts — единственное место с SQL, *.policy.ts — правила без I/O, *.pure.ts — вычисления, *.route.ts — граница HTTP. Роль видна в листинге каталога, файл не нужно открывать, чтобы понять, что в нём.
Направление зависимостей
Кто про кого знает. В GRAIN стрелки идут внутрь: граница знает про домен, домен про границу — никогда. Как только *.pure.ts импортировал *.store.ts, чистая часть перестала быть чистой.
Мок
Подделка зависимости в тесте: фальшивая база, подменённые часы. Мок — не инструмент, а симптом: если для теста правила нужна поддельная база, значит правило смешано с I/O и его пора вынести в *.policy.ts или *.pure.ts.
Единица измерения в имени
Суффикс, который сообщает, в чём измеряется число: durationSec, timeoutMs, priceCents, sizeBytes. Число без единицы в GRAIN — ошибка, потому что это целый класс продакшн-багов: одна сторона отдаёт секунды, другая ждёт миллисекунды, и обе формально правы.
Закрытый словарь
Конечный список разрешённых слов: 33 глагола, семь префиксов булевых, десять ролей файлов, пять тегов комментариев. Закрытый — значит новое слово нельзя придумать на месте, его добавляют в стандарт. Это и есть механизм, из-за которого код выглядит написанным одной рукой.
AST, линтер
AST — дерево, в которое компилятор превращает исходный текст: не «строка с символами», а «объявление функции с таким-то именем и такими-то аргументами». Линтер ходит по этому дереву и проверяет правила. Поэтому машина умеет отличить find в имени функции от слова find в комментарии.
Escape-хатч
Разрешённый способ нарушить правило осознанно: // grain:allow <правило> — <причина>. Причина обязательна — она превращает нарушение из небрежности в решение, которое можно обсудить.
Дальше: обзор стандарта · разбор эндпоинта · как включить проверку