Структуры и типы: форма предметных данных
Эта глава — про то, из чего складывается вход функции: встроенные типы, необязательные поля, состояния и вложенные записи. И про честную границу: что компилятор проверяет сам, а что остаётся приложению.
Минимальный пример
модуль «Счета»
тип «Валюта»
вариант «Рубль»
вариант «Доллар»
вариант «Евро»
объект «Строка счёта»
«номер»: строка
«сумма»: неотрицательное
«валюта»: «Валюта»
«срок оплаты»: строка
«просрочен»: признак
комментарий иногда является строкой
объект — это запись: набор полей под одним именем. Поведения у неё нет — ни
методов, ни конструктора, ни значений по умолчанию. Поле записывается как
«имя»: тип или формой имя является типом; иногда является даёт полю
право отсутствовать.
тип с вариантами — это закрытый список значений. Валюта здесь — не строка
"RUB", а один из трёх вариантов, и опечатка в имени варианта не пройдёт
проверку.
Что делает компилятор
flang check разбирает объявления и сверяет типы. Тип поля обязан быть
известен: встроенный, объявленный в модуле или подключённый через использует.
Если тип нигде не объявлен, файл не проходит:
модуль «Счета»
объект «Строка счёта»
«номер»: строка
«валюта»: «Валюта»
FLANG_UNKNOWN_NAME … неизвестный тип «Валюта»
Разобранную модель печатает flang ast — JSON с типами и функциями. Его
читают адаптеры формы, таблицы и контракта сервиса. Для минимального примера
(поля span и fts опущены):
[
{ "kind": "sum", "name": "Валюта",
"variants": [ { "name": "Рубль", "fields": [] }, { "name": "Доллар", "fields": [] }, { "name": "Евро", "fields": [] } ] },
{ "kind": "record", "name": "Строка счёта",
"fields": [
{ "name": "номер", "type": { "kind": "string" } },
{ "name": "сумма", "type": { "kind": "named", "name": "неотрицательное" } },
{ "name": "валюта", "type": { "kind": "named", "name": "Валюта" } },
{ "name": "срок оплаты", "type": { "kind": "string" } },
{ "name": "просрочен", "type": { "kind": "flag" } },
{ "name": "комментарий", "type": { "kind": "string" }, "optional": true }
] }
]
Необязательность — отдельный признак "optional": true, а не часть строки
типа.
Встроенные типы
| flang | Что это | В flang ast |
В TypeScript (flang emit --target ts) |
|---|---|---|---|
строка |
текст | string |
string |
число |
число IEEE 754, бывает и NaN |
number |
number |
неотрицательное |
целое от 0 до 2⁵³−1 | named |
number |
целое |
целое без нижней границы | named |
number |
сотых |
деньги целым числом копеек | named |
any |
признак |
да или нет |
flag |
any |
список строки |
список | list |
Array<string> |
иногда является … |
поле может отсутствовать | "optional": true |
тип поля, без null в типе |
Тип — не украшение. От него зависит, что компилятор сможет доказать. У
неотрицательное две границы, и обещание вида «итог не меньше слагаемого»
ядро выводит из одних типов аргументов. У число границ нет, и то же обещание
остаётся недоказанным.
Слова дата и деньги компилятор тоже принимает, но это не отдельные типы:
дата — та же строка ("kind": "string"), деньги — то же число
("kind": "number"). Поэтому "вчера" проходит как дата так же легко, как
"2026-07-15":
модуль «Сроки»
объект «Счёт»
«срок»: дата
тотальная функция «Срок оплаты»
принимает счёт: «Счёт»
возвращает строка
пример «любая строка проходит как дата»
дано счёт равно запись «Счёт» с «срок» равным "вчера"
ожидается "вчера"
счёт.«срок»
Календарного типа во flang 0.7.29 нет. Разбор и проверку даты делает
приложение на границе, а в модель приходит уже проверенная строка вида
ГГГГ-ММ-ДД или число дней. Встроенного типа валюты тоже нет — валюта
объявляется своим типом с вариантами, как в минимальном примере.
Необязательное поле
иногда является разрешает не задавать поле при сборке записи. Обратная
сторона: в 0.7.29 такое поле почти бесполезно внутри функций. Прочитанное
значение имеет тип «строка или ничто», и сравнить его с ничто нельзя:
FLANG_TYPE … сравнение «eq» между строка или ничто и ничто
Если правило должно вести себя по-разному при наличии и отсутствии значения, заведите тип с вариантами:
модуль «Заявки»
тип «Комментарий»
вариант «Есть» содержит текст: строка
вариант «Нет»
объект «Заявка»
«имя»: строка
«комментарий»: «Комментарий»
тотальная функция «Нужен ответ менеджера»
принимает заявка: «Заявка»
возвращает признак
пример «без комментария ответ не нужен»
дано заявка равно запись «Заявка» с «имя» равным "Анна" и «комментарий» равным (вариант «Нет»)
ожидается нет
разбор заявка.«комментарий»
случай вариант «Есть» с текст как текст
то да
случай «Нет»
то нет
иногда является остаётся полезным там, где поле только едет дальше: в форму,
в таблицу, в напечатанный код.
Состояние — это тип с вариантами
Состояние заказа — не строка и не набор признаков, а тип с вариантами. Вариант может нести свои поля: у отгруженного заказа есть накладная, у созданного её нет.
модуль «Заказы»
тип «Состояние заказа»
вариант «Создан»
вариант «Оплачен»
вариант «Отгружен» содержит «накладная»: строка
тотальная функция «Можно отгрузить»
принимает заказ: «Состояние заказа»
возвращает признак
пример «оплаченный заказ можно отгрузить»
дано заказ равно вариант «Оплачен»
ожидается да
разбор заказ
случай «Создан»
то нет
случай «Оплачен»
то да
случай вариант «Отгружен» с «накладная» как накладная
то нет
разбор обязан назвать все варианты. Уберите последний случай — и файл не
пройдёт проверку:
FLANG_MATCH_NOT_EXHAUSTIVE … разбор «Состояние заказа» не покрывает «Отгружен»
В этом и смысл. Новое состояние, о котором забыли в правиле, видно при
проверке, а не в проде. Цепочка если … иначе если по строке состояния такой
гарантии не даёт: забытый случай молча уходит в последний иначе.
Вложенная запись
Если у объекта есть составные данные — не одно значение, а несколько полей вместе, — поле получает тип другой записи:
модуль «Доставка»
объект «Адрес»
«город»: строка
«индекс»: строка
объект «Заказ»
«номер»: строка
«адрес доставки»: «Адрес»
тотальная функция «Город доставки»
принимает заказ: «Заказ»
возвращает строка
пример «город берётся из вложенной записи»
дано заказ равно запись «Заказ» с «номер» равным "ЗК-1" и «адрес доставки» равным (запись «Адрес» с «город» равным "Казань" и «индекс» равным "420000")
ожидается "Казань"
заказ.«адрес доставки».«город»
В flang ast такое поле — { "kind": "named", "name": "Адрес" }. Поля
вложенной записи туда не разворачиваются: адаптер формы сам находит запись
«Адрес» в списке types, если ему нужны её поля.
Во что превращается объект
Форму не нужно описывать второй раз — её поля уже есть в модели.
examples/spec/form-schema/schema.mjs переводит запись из flang ast в
описание формы:
export function flangFormSchema(ast, objectName) {
const records = ast.types.filter((item) => item.kind === 'record');
const record = records.find((item) => item.name === objectName.replace(/^«|»$/gu, ''));
if (!record) {
throw new Error(`в модели нет объекта «${objectName}» (есть: ${records.map((item) => item.name).join(', ')})`);
}
return {
id: `${ast.module}.${record.name}`,
title: record.name,
fields: record.fields.map((field) => {
const control = controlFor(field.type, ast);
const entry = { name: field.name, label: field.name, type: typeName(field.type), control, required: !field.optional };
if (control === 'select') {
entry.options = ast.types.find((item) => item.name === entry.type).variants.map((variant) => variant.name);
}
if (entry.type === 'неотрицательное') entry.min = 0;
return entry;
}),
};
}
Признак становится флажком, неотрицательное — числовым полем с min: 0,
поле с типом-вариантом — выбором из вариантов. Обязательность берётся из
optional. Раскладка, подсказки и доступность остаются за компонентом
интерфейса: модель убирает только дублирование имён, типов и обязательности.
Как типы попадают в TypeScript
flang emit --target ts печатает модуль, где каждая запись — тип TypeScript.
Для «Строки счёта» из invoices-table.flang:
export type StrokaSchyota = {
номер: string; контрагент: string; сумма: number; валюта: Valyuta; "срок оплаты": string; просрочен: any
};
Две вещи стоит знать заранее. признак и сотых печатаются как any, так
что TypeScript не поймает строку на месте признака. Это делает граница входа:
прогонщик flang_cli.js или проверка в сервисе. Необязательное поле
печатается своим типом без null, а фабрика записи ставит в отсутствующее
поле null.
Имена типов и функций в напечатанном коде — транслитерация имён модели
(StrokaSchyota). Под исходными именами функции доступны через
$PROGRAM.functions.
Практика в песочнице
Найдите тип «Валюта» и поле «валюта» записи «Строка счёта». Поменяйте в
примере (вариант «Рубль») на (вариант «Юань») и проверьте: компилятор
ответит, что ждал вариант типа «Валюта», а не примет значение молча.
У «Решения» два варианта, и у «Отказано» есть поле причина. Уберите в функции
«Причина отказа» случай «Разрешено» и прочитайте, что ответит проверка.
Поля «вес» и «расстояние» — разные единицы измерения, но для компилятора
оба просто неотрицательное. Подумайте, какая отдельная запись защитила бы
от перепутанных значений.
Типичные ошибки
FLANG_PARSE … поле записывается как '«имя» является типом' — строка поля
не подходит ни под «имя»: тип, ни под имя является типом. Частая причина —
пропущенное слово: сумма деньгами вместо «сумма»: деньги.
FLANG_UNKNOWN_NAME … неизвестный тип — тип поля нигде не объявлен.
Объявите его в модуле (тип с вариантами или объект) или подключите модуль,
где он объявлен.
FLANG_MATCH_NOT_EXHAUSTIVE — разбор не называет какой-то вариант.
Допишите случай, а не прячьте его за общей веткой: ради этой ошибки состояние
и делают типом.
Имя занято языком. Слова вроде с или состояние — служебные, и
параметр с таким именем даёт FLANG_PARSE … это слово занято языком. Возьмите
имя в ёлочки или переименуйте.
Чек-лист
- Я знаю встроенные типы и то, что
датаиденьги— это строка и число, а не отдельные проверки. - Состояние у меня — тип с вариантами, а разбор называет все варианты.
- Поле, от которого зависит правило, обязательное или записано типом
«Есть / Нет», а не через
иногда является. - Я умею прочитать запись в
flang astи предсказать её тип в напечатанном TypeScript.