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

Подъязык вычислений <compute>

<compute> — такая же часть языка TDC, как <switch> или <mix>, только про вычисления: он выводит значение из других значений. Он не выдумывает данные случайно (этим занимается <gen>) — он обрабатывает уже готовые значения. Пишется прямо в конфиге, внутри <sequence>, — именно так строятся настоящие контрольные суммы: контрольная цифра ИНН, номер карты по алгоритму Луна, IBAN по mod-97.

  • <gen> порождает значение (случайное, из диапазона/списка/шаблона).
  • <compute> выводит значение как чистую функцию от других значений.

<compute> читает другие последовательности через <field name="…"/> — ровно те же имена, что и в ${{…}}.

Обработчик, а не генератор

У <compute> нет своей случайности — вообще никакой. Дайте ему те же входы, и он вернёт тот же ответ, всегда. Последовательность, у которой единственный ребёнок — <compute>, полностью игнорирует seed прогона: поменяйте сид, и колонка будет байт в байт та же, потому что ничего в ней никогда не разыгрывалось.

Поэтому uniq="true" на такой последовательности запрещён (TDC218). Обработчик не может обещать уникальность: у него нет пула, из которого тянуть без возврата, нет своих колонок, чтобы их переставить, и нет костей, чтобы перебросить при совпадении — f(x) это f(x). Повторяется ли результат — свойство формулы, а не <compute>. Просите уникальность у тех <gen>-последовательностей, которые он читает, либо оберните их в <uniq>.

Все паки данных, собирающие идентификатор, устроены именно так: <gen>-соседи бросают кости, а <compute> считает контрольную цифру. Из 188 встроенных паков, использующих <compute>, ни один не обходится без <gen> рядом.

Поэтому <sequence> содержит либо одно, либо другое, но не оба сразу: <compute> рядом с <gen> — это TDC219. Вынесите <compute> в свою <sequence> и читайте разыгранную через <field name="…"/> — ровно так, как разложены паки.

Настоящий номер карты из одного прогона: пятнадцать сгенерированных цифр и цифра, выведенная из них через compute.
  • Aсгенерированные цифры
  • Bвыведенная контрольная цифра
  • Cарифметика: каждая вторая цифра удвоена, всё сложено, и добавлена цифра, доводящая сумму до кратной десяти

Труба

Читайте <compute> как трубу из трёх частей.

  1. Вход. <field name="First"/> втягивает колонку, которая уже есть.
  2. Работа. Операции вкладываются друг в друга, считаются изнутри наружу.
  3. Выход. <result> держит готовое значение, по одному на запись.

<result> обязателен, и он ровно один. Всё остальное в блоке — это либо привязка <let>, либо дерево внутри этого <result>.

Логин из двух колонок: первая буква одной, приклеенная к другой, в нижнем регистре.
  • Aколонки, которые <compute> читает через <field> — их разыграли в другом месте
  • Bоперации, каждая кормит следующую; первой выполняется самая внутренняя
  • Cготовое значение — то, что держит <result>

Вот вся форма в самом маленьком виде — логин из имени и фамилии:

<tdc>
<env count="3" seed="pipe" local="en">
<sequence name="First"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="Last"><gen type="template" value="person.lastName"/></sequence>
<sequence name="Login">
<compute>
<result>
<lower>
<concat>
<slice from="0" to="1"><field name="First"/></slice>
<field name="Last"/>
</concat>
</lower>
</result>
</compute>
</sequence>
</env>
<block><line><data>${{First}} ${{Last}} → ${{Login}}</data></line></block>
</tdc>
./run login.tdc
James Williams → jwilliams
Robert Johnson → rjohnson
John Smith → jsmith

Три операции, считаются изнутри наружу: <slice> берёт первую букву, <concat> приклеивает её к фамилии, <lower> переводит результат в нижний регистр. Ничего здесь не разыгрывается — оба имени разыграли свои <gen>, а <compute> только переставил то, что они выдали.

Слоты: дети с должностью

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

<tdc>
<env count="3" seed="slot" local="en">
<sequence name="Score"><gen type="number" value="0..2"/></sequence>
<sequence name="Grade">
<compute>
<result>
<at>
<in><list v="100,200,300"/></in>
<index><to_number><field name="Score"/></to_number></index>
</at>
</result>
</compute>
</sequence>
</env>
<block><line><data>${{Score}} → ${{Grade}}</data></line></block>
</tdc>
./run at.tdc
1 → 200
2 → 300
0 → 100

<in> и <index> — слоты тега <at>. Сами по себе они не значат ничего, больше нигде не встречаются, и от перестановки местами ничего не меняется — смысл несёт имя, а не позиция. Четырнадцать тегов на этих страницах — такие же слоты или переменные цикла, и каждый вводится вместе с тем тегом, которому принадлежит.

Тот же выбор из списка на канвасе Studio — список и позиция висят на именованных портах, а не на первом и втором ребёнке. Нажмите, чтобы открыть в полный размер.

Разбор примера — валидный ИНН

ИНН почти случаен: последняя цифра — контрольная, посчитанная из остальных. Сгенерируем 9 случайных цифр, а <compute> пусть допишет ту единственную цифру, которая делает номер валидным:

<tdc>
<env count="100" seed="demo">
<sequence name="Base">
<gen type="number" value="100000000..999999999"/> <!-- 9 случайных цифр -->
</sequence>

<sequence name="Inn">
<compute> <!-- полный ИНН -->
<let name="check">
<mod><mod>
<reduce>
<over><field name="Base"/></over>
<init><int v="0"/></init>
<do><add><acc/><multiply><current/>
<at><in><list v="2,4,10,3,5,9,4,6,8"/></in>
<index><current_index/></index></at>
</multiply></add></do>
</reduce>
<int v="11"/></mod><int v="10"/></mod>
</let>
<result><concat><field name="Base"/><var name="check"/></concat></result>
</compute>
</sequence>
</env>
<block><line><data>${{Inn}}</data></line></block>
</tdc>
./run inn.tdc
5962157388
4783743000
6257705772
2280980875
6591231869

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

Разберём по частям: <reduce> сворачивает 9 цифр в одну бегущую сумму. Каждую цифру умножаем на весовой коэффициент, который берём по её позиции из списка коэффициентов через <at> — номер позиции даёт <current_index/>. Затем mod 11, а следом mod 10 превращают сумму в итоговую контрольную цифру, и <concat> приклеивает её к базе.

Три типа значений

Любое compute-выражение вычисляется в одно из трёх значений.

ТипЧто этоЛитерал
intцелое число (64 бита)<int v="10"/>
strстрока<str v="AB"/>
listсписок из int/str<list v="2,4,10"/>

Значение литерала пишется в атрибуте v — в TDC теги не могут содержать текст между <тег> и </тег>. Дробных чисел и логических «истина/ложь» нет.

Знакомые имена, другое поведение

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

Вы пишетеЧего можно ждатьЧто даёт TDC
<divide> 7 на 23.53 — целочисленное деление, остаток отброшен
<divide> 1 на 30.330
<mod> -7 на 3-1, как в C, Java и JavaScript2 — остаток никогда не отрицательный
<list v="a,b"/>список из двух строкошибку: "a" is not an integer
<replace from="[ab]"/>регулярное выражениебуквальное совпадение, поэтому не заменится ничего
<replace from="a"/> на bananaпервую abXnXnX — все вхождения
<equals> для 5 и "5"разные типы, значит не равныравны
<slice from="99"> на abcошибкупустую строку, молча
<pad width="2"> на 123451212345width это минимум, а не обрезка
<each> там, где ждут строкустрокуошибку: cannot use a list where a string is expected

Три из них заслуживают больше, чем строку таблицы.

<let> и <var> — не два вида переменной

Это объявление и чтение. <let name="x"> связывает имя; <var name="x"/> читает его обратно. В большинстве языков программирования и let, и var объявляют переменную — именно поэтому пара здесь сбивает с толку.

Отсюда три правила, и движок следит за всеми тремя:

ПравилоЧто получите, если нарушить
Имя должно быть связано до того, как его прочитаютTDC182: <var name="x"> is not bound by an enclosing <let>
Имя связывается один раз и не перепривязываетсяTDC185: <let name="x"> shadows an outer binding of the same name
Привязка видна только внутри своего слота<let> внутри <do> снаружи не виден — снова TDC182

Проще говоря: <let> считает значение один раз и даёт ему имя, а <var> — это способ попросить это значение снова, не пересчитывая всё заново. После того как имя дано, значение уже не меняется — ради этого его и называют. Ровно так же считают на бумаге: пусть s — это оценка числом, записали один раз и дальше пользуются до конца.

<divide> выбрасывает остаток

Эта ловушка стоит дороже всех, потому что ничего не ломается вслух. Процент, посчитанный как <divide> части на целое, равен 0 в каждой записи, и колонка выглядит правдоподобно, пока кто-нибудь не проверит. Умножайте сначала, делите в конце — или держите значение в целых единицах: в копейках, а не в рублях.

Деление на ноль не пропускается молча, а отклоняется: <divide>: the divisor (second child) must not be zero.

Значение из <field> — строка

<field name="Score"/> отдаёт строку, даже если в колонке цифры. Арифметике нужно число, и на границе между ними стоит <to_number>. Забудете — ошибка назовёт тег, которому достался не тот тип.

Обратный переход происходит сам: число, попавшее в <concat>, становится своими цифрами.

Семейства тегов

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

Литералы и ссылки

Значения, с которых вы стартуете, и имена, которые вы даёте промежуточным результатам.

ТегЧто делает
<int v="10"/>целое число (v — десятичное, можно с ведущим -)
<str v="AB"/>строка
<list v="2,4,10"/>список целых через запятую
<field name="X"/>значение последовательности X в области видимости — как ${{X}}
<var name="X"/>значение, связанное объемлющим <let name="X">
<let name="X">…назвать промежуточный результат для соседних тегов
<current/>текущий элемент (только внутри <do>)
<current_index/>номер текущего элемента, отсчёт с нуля
<acc/>накопитель (только внутри <reduce><do>)

Арифметика

Целочисленная математика. <add> суммирует своих детей, <subtract> — это первое минус сумма остальных, <multiply> — произведение, <divide> — целочисленное деление, а <mod>всегда неотрицательный остаток. Рядом с ними — <to_number> (строка из цифр → целое) и <encode> (символ → число в некоторой системе счисления).

Списки и перебор

<each> применяет <do> к каждому элементу списка, <reduce> сворачивает список в одно значение через <acc>, <join> превращает список в строку, <at> берёт элемент по номеру, а <length> измеряет длину строки или списка. Обёртки <over> / <do> / <init> / <in> / <index> явно указывают, какой ребёнок какую роль играет.

Строки и форматирование

<concat> склеивает части; <upper> / <lower> / <capitalize> / <title> меняют регистр; а <mask>, <slice>, <replace>, <trim>, <group> и <pad> преобразуют строку. То же форматирование доступно и через атрибуты <gen> и фильтры ${{X|…}}.

Условия

<choose> берёт первый <when>, чей <test> истинен, иначе — <otherwise> (он обязателен). Предикаты живут только внутри <test> и не возвращают значения: <equals>, <greater_than>, <less_than> (сравнивают два целых) и <is_digit> (символ 09).

<choose>
<when><test><greater_than><var name="d"/><int v="9"/></greater_than></test>
<then><subtract><var name="d"/><int v="9"/></subtract></then></when>
<otherwise><var name="d"/></otherwise>
</choose>

В пакетах данных: параметры и <valid>

Встроенные генераторы идентификаторов — это пакеты данных .tdc, где <compute> стоит рядом с <gen>. Автору пакета помогают два механизма.

Параметры

Любой атрибут на вызывающем <gen type="template" …> (кроме служебных type / value / local) переопределяет одноимённую локальную <sequence> в пакете константой. Автор пакета объявляет параметр как последовательность со значением по умолчанию, а вызывающий может его зафиксировать:

<gen type="template" value="russia.tax.inn_org" tax_office="7712"/>

Теперь каждый ИНН начинается с фиксированного кода налоговой (tax_office), а контрольная цифра по-прежнему получается верной — <compute> пересчитывает её от любой полученной базы.

./run inn-org.tdc
7712702700
7712682927
7712220601

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

Отклонить и повторить — <valid>

Иногда вычисленная контрольная цифра непредставима в целевом формате или недопустима для предметной области. Например, польский NIP не выдаётся, если контрольная равна 10; такие строки приходится отбрасывать. Пакет добавляет один предикат <valid>, и движок перегенерирует базу, пока она не пройдёт проверку (с предохранителем, чтобы невозможное условие не зациклилось навечно):

<sequence name="check"><compute><result> …контрольная цифра… </result></compute></sequence>
<valid><less_than><to_number><field name="check"/></to_number><int v="10"/></less_than></valid>

Среди тысяч строк ни в одной контрольная не будет равна 10 — каждый выданный NIP состоит только из чистых цифр.

./run nip.tdc
7027013633
6829260874
2206093132

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

Ограничения

  • Никаких строк-выражений — каждая операция это отдельный тег.
  • Только целые числа — переполнение 64 бита это ошибка, а не «тихое» переполнение; дробных чисел и логических значений нет.
  • Только ограниченные циклы (<each>, <reduce> по конечному входу) — язык всегда завершается.
  • Имена тегов пишутся через _ (как before_block): current_index, to_number, greater_than, less_than, is_digit.
  • Ошибки в дереве ловятся до запуска (коды TDC180TDC187): неизвестный тег, несвязанная <var>, <choose> без <otherwise> и т. п.

Смотрите также