Исполняемые спецификации на flang Структуры и типы: форма предметных данных
0%

Структуры и типы: форма предметных данных

Структуры и типы: форма предметных данных

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

Минимальный пример

модуль «Счета»

тип «Валюта»
  вариант «Рубль»
  вариант «Доллар»
  вариант «Евро»

объект «Строка счёта»
  «номер»: строка
  «сумма»: неотрицательное
  «валюта»: «Валюта»
  «срок оплаты»: строка
  «просрочен»: признак
  комментарий иногда является строкой

объект — это запись: набор полей под одним именем. Поведения у неё нет — ни методов, ни конструктора, ни значений по умолчанию. Поле записывается как «имя»: тип или формой имя является типом; иногда является даёт полю право отсутствовать.

тип с вариантами — это закрытый список значений. Валюта здесь — не строка "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.

Кейсы каталога по этой теме

Дальше: правила, порядок и обещания

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов
Дальше