DigitableCourses

Зачем этот язык

Одна задача, два ответа: 130 000 молча — или отказ с именем нарушенного правила.

Рассмотрим пример: премия — 30 процентов оклада за выполненный план, ещё 25 — за переработку от 20 часов, 20 000 — за период без инцидентов. Регламент компании ограничивает премию сотней тысяч. Оклад 200 000, план выполнен, переработка 24 часа, инцидентов нет.

привычноTypeScript
type Employee = {
  salary: number;        // оклад
  overtimeHours: number; // переработка, часов
  planDone: boolean;     // план выполнен
  incidents: number;     // инцидентов за период
};

// Регламент: премия не может превысить 100 000. В коде этого нет.
export function bonus(e: Employee): number {
  let total = 0;
  if (e.planDone) total += e.salary * 0.30;
  if (e.overtimeHours >= 20) total += e.salary * 0.25;
  if (e.incidents === 0) total += 20000;
  return total;
}

bonus({ salary: 200000, overtimeHours: 24, planDone: true, incidents: 0 });
// → 130000
flangpatterns/refuse.fts
// Паттерн «Отказ вместо молчания»: политика, записанная свойством, действует на
// каждом входе, а не только на тех, что попали в примеры. Оклад 200 000 с
// выполненным планом, переработкой и без инцидентов даёт 130 000 при потолке
// 100 000 — модель обязана отказать, а не выдать число.
категория «Мотивация»

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

  утилита «Рассчитать премию»
    принимает Сотрудник
    возвращает деньги
    начинает с 0

    правило «План выполнен»
      если «план выполнен» равен да
      то добавить 30 процентов от поля оклад

    правило «Переработка»
      если «переработка часов» не меньше 20
      то добавить 25 процентов от поля оклад

    правило «Без инцидентов»
      если инциденты равен 0
      то добавить 20000

    свойство «Премия ограничена»
      результат не больше 100000

    пример «План без переработки»
      дано оклад равен 100000
      дано «переработка часов» равен 0
      дано «план выполнен» равен да
      дано инциденты равен 3
      ожидается результат равен 30000

    пример «Тихий месяц без плана»
      дано оклад равен 100000
      дано «переработка часов» равен 0
      дано «план выполнен» равен нет
      дано инциденты равен 0
      ожидается результат равен 20000

Слева ошибки нет. Функция делает ровно то, что написано, и возвращает 130 000. Ограничение существует — оно в регламенте, а не в коде, и ни один из тестов, написанных на типичные входы, эту комбинацию не содержит. Неверное число уедет в расчётный лист, и обнаружат его не тестом, а жалобой.

Справа то же самое плюс одна строкасвойство «Премия ограничена». Это не проверка на выходе и не Math.min: свойство — утверждение обо ВСЕХ входах сразу. На том же входе модель не возвращает ничего и говорит, что именно нарушено:

нарушено свойство «Премия ограничена» утилиты «Рассчитать премию»

Разница не в аккуратности программиста, а в том, кому принадлежит утверждение. Слева ограничение знает человек; справа — компилятор, и он проверяет его на каждом вычислении, включая то, которого нет ни в одном примере.

ПесочницаПроверьте сами: откройте модель на вкладке «Выполнить»Оклад 200 000, переработка 24, план выполнен, инцидентов 0 — и посмотрите на отказ. Считается в вашей вкладке, ни один вызов не уходит на сервер.

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

Путь

8 ступеней: от первой модели до задачи, где отказ — правильный ответ.

Каждая ступень — одно задание. Вердикт выносит не сверка со строкой, а исполнение: модель разбирается компилятором, прогоняются её СОБСТВЕННЫЕ примеры и приёмочные входы задания. Вход, помеченный как отказной, обязан упереться в свойство — модель, которая просто считает, его не пройдёт.

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

  1. Первая модель: цена билета из двух надбавок

    Написать модель целиком: объект с полями, утилиту, правило, свойство и пример — и увидеть, как компилятор её принимает.

    Рассмотрим пример: цена билета. Это первая модель целиком — объект, утилита, правило, свойство и пример. Опишите объект «Билет» с полями «базовый тариф» (деньги), «мест багажа» (число) и «выбор места» (признак) и утилиту «Рассчитать цену», которая начинает с 0 и складывает: сам базовый тариф — 100 процентов от поля «базовый тариф», если он больше нуля; 1500 за каждый случай, когда мест багажа не меньше одного; 700 при выбранном месте. Запишите свойством, что цена не бывает отрицательной, и приложите хотя бы один собственный пример.

    Паттерн: Вклады складываются Проверяется исполнением: 3 приёмочных входов Кейс: Рассчитать цену билета (Путешествия)

    Решить и проверить

  2. Скидка корзины: три независимых вклада и потолок

    Заменить лестницу if / else if набором независимых вкладов, а верхний предел вынести из правил в свойство.

    Рассмотрим пример: скидка корзины в магазине. Опишите объект «Корзина» с полями «сумма» (деньги), «позиции» (число) и «промокод предъявлен» (признак) и утилиту «Рассчитать скидку», которая начинает с 0 и складывает: 5 процентов суммы при сумме не меньше 5000; 300 при десяти и более позициях; 200 при предъявленном промокоде. Потолок скидки — 5000; запишите его свойством, а не четвёртым правилом. Приложите хотя бы один собственный пример.

    Паттерн: Вклады складываются Проверяется исполнением: 4 приёмочных входов, из них 1 обязан упереться в свойство Кейс: Рассчитать скидку корзины (Ритейл)

    Решить и проверить

  3. Тариф перевозки: зоны без дыр и без нахлёстов

    Разметить числовой диапазон взаимоисключающими правилами без дыр и без нахлёстов и проверить себя ровно на границах.

    Рассмотрим пример: тариф перевозки. Опишите объект «Рейс» с полями «километров» (число), «вес» (число) и «хрупкий груз» (признак) и утилиту «Рассчитать перевозку», которая начинает с 0. Три взаимоисключающих правила задают базу по расстоянию: 500 при пробеге меньше 200; 900 при пробеге от 200 до 1000; 1600 при пробеге от 1000. Сверху добавляются надбавки: 350 при весе больше 20 и 250 за хрупкий груз. Свойством запишите, что тариф не опускается ниже 500. Приложите хотя бы один собственный пример.

    Паттерн: Разбиение без дыр Проверяется исполнением: 5 приёмочных входов Кейс: Рассчитать стоимость доставки (Логистика)

    Решить и проверить

  4. Итог курса: веса работ и порог посещаемости

    Отличать правило от свойства: одно вносит вклад, другое утверждает что-то обо всех входах сразу.

    Рассмотрим пример: итоговая оценка за курс. Опишите объект «Работы» с полями «теория», «практика» и «посещаемость» (числа) и утилиту «Рассчитать итог», которая начинает с 0 и складывает: 40 процентов теории, если теория больше нуля; 50 процентов практики, если практика больше нуля; 10 баллов, если посещаемость не меньше 80. Итог не может превышать 100 — запишите это свойством, а не правилом.

    Паттерн: Свойство вместо ещё одного правила Проверяется исполнением: 3 приёмочных входов Кейс: Рассчитать итог курса (Образование)

    Решить и проверить

  5. Приоритет тикета: SLA-правила и признак

    Работать с признаком и порогом: условие на «да / нет» и условие «не меньше» в одной утилите.

    Рассмотрим пример: приоритет тикета в поддержке. Опишите объект «Тикет» с полями «влияние» (число), «клиентов затронуто» (число) и «платный тариф» (признак) и утилиту «Рассчитать приоритет», которая начинает с 0 и складывает: 50 при влиянии не меньше 3, 30 при более чем ста затронутых клиентах, 20 при платном тарифе. Приоритет не превышает 100.

    Паттерн: Вклады складываются Проверяется исполнением: 3 приёмочных входов Кейс: Рассчитать приоритет тикета (Поддержка клиентов)

    Решить и проверить

  6. Кредитный лимит: правила вместо ветвлений

    Считать долю от поля вместо магического числа и поймать свойством комбинацию входов, которой нет ни в одном примере.

    Рассмотрим пример: кредитный лимит по заявке. Опишите объект «Заявка» с полями «доход» (деньги), «стаж» (число) и «риск-класс» (число) и утилиту «Рассчитать лимит», которая начинает с 0 и складывает: базовый лимит — 300 процентов дохода при доходе больше нуля; надбавку за стаж — ещё 100 процентов дохода при стаже не меньше 36; премию за низкий риск — 50 000 при риск-классе не больше 1. Ограничьте результат свойством «не больше 1 000 000» и приложите хотя бы один собственный пример.

    Паттерн: Свойство вместо ещё одного правила Проверяется исполнением: 4 приёмочных входов, из них 1 обязан упереться в свойство Кейс: Рассчитать кредитный лимит (Банкинг)

    Решить и проверить

  7. Комиссия платежа: правило-выключатель и его место в порядке

    Назначать результат, а не добавлять к нему, и понимать, почему у правила-выключателя есть единственное верное место в порядке.

    Рассмотрим пример: комиссия платежа. Опишите объект «Транзакция» с полями «сумма» (деньги), «карта иностранного банка» (признак) и «внутренний перевод» (признак) и утилиту «Рассчитать комиссию», которая начинает с 0 и складывает: 1 процент суммы при сумме больше нуля и 100 за карту иностранного банка. Внутренний перевод бесплатен всегда — это не вклад, а назначение результата: «то результат равен 0». Потолок комиссии 5000 запишите свойством. Приложите хотя бы один собственный пример.

    Паттерн: Правило-выключатель Проверяется исполнением: 5 приёмочных входов, из них 1 обязан упереться в свойство Кейс: Рассчитать комиссию платежа (Платежи)

    Решить и проверить

  8. Страховая премия: модель, которая обязана отказать

    Написать модель, которая на честном входе ОТКАЗЫВАЕТСЯ считать, — и объяснить коллеге, почему отказ здесь лучше числа.

    Рассмотрим пример: страховая премия по полису. Опишите объект «Полис» с полями «страховая сумма» (деньги), «возраст объекта» (число), «убытков за год» (число) и «франшиза оформлена» (признак) и утилиту «Рассчитать премию», которая начинает с 0 и складывает: 2 процента страховой суммы при страховой сумме больше нуля; ещё 1 процент при возрасте объекта не меньше 10; 15 000 при двух и более убытках за год; минус 5000 при оформленной франшизе. Тарифный комитет ограничил премию сотней тысяч — запишите это свойством. Приложите хотя бы один собственный пример.

    Паттерн: Отказ вместо молчания Проверяется исполнением: 4 приёмочных входов, из них 1 обязан упереться в свойство Кейс: Рассчитать страховую премию (Страхование)

    Решить и проверить

Паттерны решений

5 формы решения — то, чего не найти ни в справочнике, ни в словаре.

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

Каждая модель — файл в static/fts/models/patterns/. Все пять компилируются, несут свойство и сходятся на своих примерах: не сойдётся хоть одна — падает сборка (checkPatterns в scripts/build-fts-highlight.mjs). Домен паттерна нигде не совпадает с доменом задания той же ступени — иначе паттерн был бы ответом, а не образцом.

Вклады складываются

Задача. Несколько независимых условий влияют на одно число.

Как обычно. Лестница if / else if, где каждая новая ветка обязана знать про все предыдущие: порядок значим, а добавление четвёртого условия требует перечитать три первых.

Как здесь. Каждое условие — отдельное правило со своим вкладом. Правила не знают друг о друге, порядок записи на результат не влияет, новое условие добавляется строкой.

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

модельpatterns/accumulate.fts
// Паттерн «Вклады складываются»: независимые правила вместо лестницы if/else.
// Каждое правило отвечает за свой вклад и ничего не знает об остальных, поэтому
// новое условие добавляется строкой, а не переписыванием ветвления.
категория «Подписки»

  объект Подписка
    «цена тарифа» является деньгами
    «мест в команде» является числом
    «годовая оплата» является признаком

  утилита «Рассчитать доплату»
    принимает Подписка
    возвращает деньги
    начинает с 0

    правило «Большая команда»
      если «мест в команде» не меньше 25
      то добавить 10 процентов от поля «цена тарифа»

    правило «Приоритетная поддержка»
      если «цена тарифа» не меньше 50000
      то добавить 5000

    правило «Скидка за год вперёд»
      если «годовая оплата» равен да
      то добавить -3000

    свойство «Доплата не уходит в минус глубже скидки»
      результат не меньше -3000

    пример «Маленькая команда на месячной оплате»
      дано «цена тарифа» равна 20000
      дано «мест в команде» равно 5
      дано «годовая оплата» равен нет
      ожидается результат равен 0

    пример «Все три вклада сразу»
      дано «цена тарифа» равна 50000
      дано «мест в команде» равно 40
      дано «годовая оплата» равен да
      ожидается результат равен 7000

Разбиение без дыр

Задача. Числовой диапазон разбит на зоны, и в каждой своя ставка.

Как обычно. Цепочка if (x < 150) … else if (x < 600) … else …. Работает, пока кто-нибудь не заменит одно условие на > — и тогда ровно одно значение на границе проваливается мимо всех веток.

Как здесь. Взаимоисключающие правила, вместе покрывающие всю ось. Базовую ставку вносят они сами, а не строка начинает с: начальное значение видно только там, где не сработало ни одно правило, и свойством такую нижнюю границу не проверить.

Что это покупает. Нижнюю границу можно записать свойством и проверять на КАЖДОМ входе, а не только на тех, что попали в примеры. Границы зон проверяются примерами прямо в модели.

модельpatterns/partition.fts
// Паттерн «Разбиение без дыр»: взаимоисключающие правила покрывают весь
// диапазон, и базовую ставку вносят они сами, а не «начинает с». Начальное
// значение видно только там, где не сработало ни одно правило, — а значит,
// нижнюю границу, записанную в «начинает с», свойство не проверяет.
категория «Энергетика»

  объект Потребление
    «киловатт-часов» является числом
    «ночной тариф» является признаком

  утилита «Рассчитать ставку»
    принимает Потребление
    возвращает деньги
    начинает с 0

    правило «Социальная норма»
      если «киловатт-часов» меньше 150
      то добавить 400

    правило «Обычное потребление»
      если «киловатт-часов» не меньше 150
      и «киловатт-часов» меньше 600
      то добавить 700

    правило «Повышенное потребление»
      если «киловатт-часов» не меньше 600
      то добавить 1200

    правило «Ночная льгота»
      если «ночной тариф» равен да
      то добавить -100

    свойство «Ставка не ниже социальной за вычетом льготы»
      результат не меньше 300

    пример «Ниже социальной нормы»
      дано «киловатт-часов» равно 90
      дано «ночной тариф» равен нет
      ожидается результат равен 400

    пример «Ровно на границе нормы»
      дано «киловатт-часов» равно 150
      дано «ночной тариф» равен нет
      ожидается результат равен 700

    пример «Повышенное потребление ночью»
      дано «киловатт-часов» равно 900
      дано «ночной тариф» равен да
      ожидается результат равен 1100

Свойство вместо ещё одного правила

Задача. У результата есть предел, записанный в регламенте.

Как обычно. Math.min(total, LIMIT) в конце функции — или, чаще, ничего: предел живёт в регламенте, а в коде его нет вовсе.

Как здесь. свойство — утверждение обо всех входах сразу. Оно не правит результат и не прячет проблему: вход, на котором предел нарушен, получает отказ с именем нарушенного свойства.

Что это покупает. Правило проверяется только там, куда попал пример. Свойство проверяется на каждом вычислении, включая комбинацию входов, которой ни в одном примере нет, — а именно такие комбинации и доходят до продакшена.

модельpatterns/property.fts
// Паттерн «Свойство вместо ещё одного правила»: предел — утверждение обо ВСЕХ
// входах сразу, а не четвёртая ветка. Правило срабатывает только там, куда
// попал пример; свойство проверяется на каждом вычислении, включая комбинацию,
// которой ни в одном примере нет.
категория «Недвижимость»

  объект Сделка
    «цена объекта» является деньгами
    «срочная продажа» является признаком
    «показов проведено» является числом

  утилита «Рассчитать вознаграждение»
    принимает Сделка
    возвращает деньги
    начинает с 0

    правило «Базовое вознаграждение»
      если «цена объекта» больше 0
      то добавить 3 процента от поля «цена объекта»

    правило «Срочная продажа»
      если «срочная продажа» равен да
      то добавить 2 процента от поля «цена объекта»

    правило «Много показов»
      если «показов проведено» не меньше 20
      то добавить 30000

    свойство «Вознаграждение ограничено»
      результат не больше 500000

    пример «Обычная сделка»
      дано «цена объекта» равна 5000000
      дано «срочная продажа» равен нет
      дано «показов проведено» равно 4
      ожидается результат равен 150000

    пример «Срочная продажа с длинной экспозицией»
      дано «цена объекта» равна 5000000
      дано «срочная продажа» равен да
      дано «показов проведено» равно 25
      ожидается результат равен 280000

Правило-выключатель

Задача. Одно условие отменяет весь расчёт целиком.

Как обычно. Ранний return 0 в начале функции. Отменяет он ровно то, что написано ниже, — и молча перестаёт что-либо отменять, когда кто-то добавляет строку выше.

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

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

модельpatterns/override.fts
// Паттерн «Правило-выключатель»: «то результат равен» не добавляет вклад, а
// назначает результат. Правила применяются сверху вниз, поэтому выключатель
// ставится ПОСЛЕ накопления: поставленный первым, он был бы затёрт следующими
// вкладами и выглядел бы сработавшим, ничего не изменив.
категория «Телеком»

  объект Абонент
    «пакет минут» является числом
    «мобильный интернет» является признаком
    «корпоративный договор» является признаком

  утилита «Рассчитать абонентскую плату»
    принимает Абонент
    возвращает деньги
    начинает с 0

    правило «Базовый пакет»
      если «пакет минут» больше 0
      то добавить 300

    правило «Большой пакет минут»
      если «пакет минут» не меньше 1000
      то добавить 250

    правило «Мобильный интернет»
      если «мобильный интернет» равен да
      то добавить 400

    правило «Корпоративный договор платит компания»
      если «корпоративный договор» равен да
      то результат равен 0

    свойство «Плата неотрицательна»
      результат не меньше 0

    пример «Частный абонент с интернетом»
      дано «пакет минут» равен 500
      дано «мобильный интернет» равен да
      дано «корпоративный договор» равен нет
      ожидается результат равен 700

    пример «Большой пакет»
      дано «пакет минут» равен 1500
      дано «мобильный интернет» равен да
      дано «корпоративный договор» равен нет
      ожидается результат равен 950

    пример «Корпоративный абонент не платит сам»
      дано «пакет минут» равен 1500
      дано «мобильный интернет» равен да
      дано «корпоративный договор» равен да
      ожидается результат равен 0

Отказ вместо молчания

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

Как обычно. Функция возвращает число. Какое получилось — такое и возвращает: ограничение записано в регламенте, а тесты писались на типичные входы, и этой комбинации среди них нет.

Как здесь. Ограничение записано свойством, и модель ОТКАЗЫВАЕТСЯ считать, называя нарушенное свойство по имени.

Что это покупает. Разница между «система выдала неверное число» и «система отказалась выдать число». Первое обнаруживают через месяц по жалобе, второе — на этапе проверки.

модельpatterns/refuse.fts
// Паттерн «Отказ вместо молчания»: политика, записанная свойством, действует на
// каждом входе, а не только на тех, что попали в примеры. Оклад 200 000 с
// выполненным планом, переработкой и без инцидентов даёт 130 000 при потолке
// 100 000 — модель обязана отказать, а не выдать число.
категория «Мотивация»

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

  утилита «Рассчитать премию»
    принимает Сотрудник
    возвращает деньги
    начинает с 0

    правило «План выполнен»
      если «план выполнен» равен да
      то добавить 30 процентов от поля оклад

    правило «Переработка»
      если «переработка часов» не меньше 20
      то добавить 25 процентов от поля оклад

    правило «Без инцидентов»
      если инциденты равен 0
      то добавить 20000

    свойство «Премия ограничена»
      результат не больше 100000

    пример «План без переработки»
      дано оклад равен 100000
      дано «переработка часов» равен 0
      дано «план выполнен» равен да
      дано инциденты равен 3
      ожидается результат равен 30000

    пример «Тихий месяц без плана»
      дано оклад равен 100000
      дано «переработка часов» равен 0
      дано «план выполнен» равен нет
      дано инциденты равен 0
      ожидается результат равен 20000