Из Python, Go и shell: работающие клиенты, а не декларации
Модель на flang не привязана к Node.js. Граница между ней и остальным кодом —
не библиотека, а процесс: любой язык, который умеет запустить команду и
прочитать её вывод, может посчитать функцию модели. Дальше — три работающих
клиента: Python, Go и shell. Контракт один, результат один. Все три лежат в
examples/spec/clients/ и правда запускаются.
Минимальный пример
Модель — расчёт скидки из static/spec/models/order-discount.flang, та же,
что у HTTP-сервиса из главы про Node.js. Для вызова из командной строки в ней
есть функция с двумя скалярными аргументами:
тотальная функция «Скидка по сумме»
принимает сумма: неотрицательное, постоянный: признак
возвращает число
пример «тот же расчёт без записи на входе»
дано сумма равно 20000
дано постоянный равно да
ожидается 3000
«Рассчитать скидку» от (запись «Покупка» с «сумма» равным сумма и «постоянный клиент» равным постоянный)
Она собирает запись «Покупка» и зовёт «Рассчитать скидку». Вызов:
$ flang run static/spec/models/order-discount.flang --function "Скидка по сумме" --args '{"сумма": 20000, "постоянный": true}' --trust
на веру: доказанность не считалась — запуск по ключу --trust
3000
Что делает компилятор
flang run считает одну функцию сам: ни Node, ни компилятор C для этого не
нужны. Перед вычислением он сверяет аргументы с объявленными типами и выносит
вердикт о доказанности.
Вердикт здесь важен. В модели три обещания из четырёх держатся только на
примерах: в отчёте flang check --proof они названы «сетка N» — проверено на N
значениях из примеров, это не доказательство. Недоказанную программу
flang run без явного согласия не считает и выходит кодом 3:
$ flang run static/spec/models/order-discount.flang --function "Скидка по сумме" --args '{"сумма": 20000, "постоянный": true}'
не доказано: утверждений 4: доказано 1, сетка 3, на веру 0 — запуск только по явному согласию: --на-веру
Ключ --trust даёт согласие на один прогон. Ключ unproven в .flangrc —
на все. Клиенты ниже передают --trust явно, чтобы согласие было видно в коде.
Граница через JSON
Контракт flang run простой:
- аргументы —
--args, плоский JSON скаляров: число, строка, да/нет. Записи и списки сюда не передать, поэтому в модели и есть «Скидка по сумме»; - значение — в stdout, одной строкой;
- вердикт о доказанности и ошибки — в stderr, ошибка с кодом
FLANG_…; - код возврата:
0— посчитано,1— ошибка во входе или в программе,2— не разобран сам вызов,3— программа не доказана и запуск без согласия отклонён.
Устройство самой модели тоже доступно машине без парсера flang: flang ast
печатает разобранную программу JSON-ом — объекты с полями и типами, функции
с параметрами и примерами. Из него HTTP-сервис строит контракт, а форма —
свою схему.
Поставить flang:
curl -fsSL https://raw.githubusercontent.com/digitable-lol/flang/main/install.sh | sh
Клиенты берут путь к двоичному из переменной FLANG, по умолчанию — flang
из PATH.
Клиент на Python
examples/spec/clients/python/calculate_discount.py — только subprocess и
json из стандартной библиотеки:
def calculate_discount(amount: int, loyal: bool, model: Path = DEFAULT_MODEL) -> float:
"""Вызывает функцию модели «Скидка по сумме» и возвращает число."""
command = [
os.environ.get("FLANG", "flang"), "run", str(model),
"--function", FUNCTION,
"--args", json.dumps({"сумма": amount, "постоянный": loyal}, ensure_ascii=False),
"--trust",
]
completed = subprocess.run(command, capture_output=True, text=True, env={**os.environ, "LC_ALL": "C.UTF-8"})
if completed.returncode != 0:
raise RuntimeError(describe_failure(completed))
return float(completed.stdout.strip())
Ошибка не глотается. describe_failure ищет в stderr первую строку с кодом
FLANG_… — это причина отказа словами компилятора — и показывает её вместе с
кодом возврата.
Запуск и вывод:
$ python3 calculate_discount.py
сумма=20000 постоянный_клиент=False -> скидка=2000
$ python3 calculate_discount.py --сумма 20000 --постоянный-клиент
сумма=20000 постоянный_клиент=True -> скидка=3000
$ python3 calculate_discount.py --сумма -5
ошибка: flang завершился с кодом 1: FLANG_TYPE: вызов функции «Скидка по сумме»: аргумент «сумма»: -5 вне неотрицательное
Клиент на Go
examples/spec/clients/go/main.go использует только os/exec и
encoding/json:
func calculateDiscount(sum int, loyal bool, model string) (float64, error) {
arguments, err := json.Marshal(map[string]any{"сумма": sum, "постоянный": loyal})
if err != nil {
return 0, err
}
bin := os.Getenv("FLANG")
if bin == "" {
bin = "flang"
}
cmd := exec.Command(bin, "run", model, "--function", functionName, "--args", string(arguments), "--trust")
cmd.Env = append(os.Environ(), "LC_ALL=C.UTF-8")
var out, errOut bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &errOut
if err := cmd.Run(); err != nil {
return 0, fmt.Errorf("flang: %v: %s", err, reason(errOut.String()))
}
return strconv.ParseFloat(strings.TrimSpace(out.String()), 64)
}
stdout и stderr пишутся в разные буферы: причина отказа не потеряется, даже если процесс завершился ошибкой. Вывод:
$ go run main.go
сумма=20000 постоянный_клиент=false -> скидка=2000
$ go run main.go -sum 20000 -loyal
сумма=20000 постоянный_клиент=true -> скидка=3000
Клиент на shell
examples/spec/clients/shell/discount.sh — bash и jq. JSON аргументов
собирает jq: склеивать его строками в bash легко сломать на кавычках и
юникоде в сумма.
ARGS="$(jq -cn --argjson sum "$SUM" --argjson loyal "$LOYAL" '{"сумма": $sum, "постоянный": $loyal}')"
set +e
LC_ALL=C.UTF-8 "$FLANG" run "$MODEL" --function "Скидка по сумме" --args "$ARGS" --trust >"$STDOUT_FILE" 2>"$STDERR_FILE"
STATUS=$?
set -e
if [[ $STATUS -ne 0 ]]; then
# Первая строка с кодом FLANG_… — причина отказа словами компилятора.
REASON="$(grep -m1 'FLANG_' "$STDERR_FILE" || head -n1 "$STDERR_FILE")"
echo "ошибка (код $STATUS): $REASON" >&2
exit "$STATUS"
fi
set -e на время вызова снимается намеренно: иначе скрипт вышел бы раньше,
чем прочитал stderr и показал причину. Вывод:
$ ./discount.sh
сумма=20000 постоянный_клиент=false -> скидка=2000
$ ./discount.sh 20000 true
сумма=20000 постоянный_клиент=true -> скидка=3000
$ ./discount.sh -5 true
ошибка (код 1): FLANG_TYPE: вызов функции «Скидка по сумме»: аргумент «сумма»: -5 вне неотрицательное
Через HTTP вместо CLI
Если запуск процесса на каждый вызов дорог, есть долгоживущий HTTP-сервис.
examples/spec/discount-api так и устроен: примеры модели прогоняются один
раз при старте, модель печатается в JavaScript, дальше Node.js только
маршрутизирует запросы.
npm run spec:api
curl -s localhost:8788/discount -d '{"сумма":20000,"постоянный клиент":true}'
# {"discount": 3000}
Ошибка входа приходит кодом 400 с причиной:
{
"error": "вход не подходит к «Покупке»: «сумма»: ждали неотрицательное целое",
"code": "INPUT"
}
Любой клиент выше переписывается на requests, net/http или curl вместо
запуска процесса. Выбор между CLI и HTTP — выбор между простотой (процесс на
вызов, ничего не держать живым) и пропускной способностью (модель готовится
один раз, дальше только сеть).
Третий способ: напечатать модель в свой язык
У обоих способов выше рядом с вашим кодом что-то живёт: двоичный flang или
сервис на Node. Иногда этого нельзя. Тогда модель печатается в целевой язык
заранее, командой flang emit. Целей десять:
$ flang emit static/spec/models/order-discount.flang --target zzz
flang emit: цели «zzz» у этой сборки flang нет — целей здесь ДЕСЯТЬ — «c», «cpp», «go», «rust», «java», «js», «ts», «elixir», «python», «csharp».
Печать в Python:
$ flang emit static/spec/models/order-discount.flang --target python --out printed
…
напечатано файлов 4, байт 109125, в printed
…
$ ls printed
Makefile flang_cli.py flang_runtime.py prodazhi.py
Модуль prodazhi.py работает без зависимостей. Функции модели в нём —
обычные функции Python, с приставкой роли в имени:
import prodazhi, flang_runtime as rt
ctx = prodazhi.new_context()
p = prodazhi.rec_pokupka(rt.number(20000), rt.flag(True))
print(prodazhi.fn_rasschitat_skidku(ctx, p))
# <flang число: 3000>
Приставка роли (fn_ у функции, rec_ у конструктора записи) нужна, чтобы
объект и функция с одинаковым именем не столкнулись после транслитерации.
Рядом лежит прогонщик flang_cli.py. Запрос — одна строка JSON, ответ — тоже
одна строка:
$ echo '{"fn":"Рассчитать скидку","args":[{"r":[["сумма",{"n":"20000"}],["постоянный клиент",true]]}]}' | python3 flang_cli.py prodazhi
{"ok":true,"value":{"n":"3000"}}
Значения в протоколе размечены: число едет строкой ({"n":"20000"}), чтобы не
потерять -0, NaN и бесконечность; строка — {"s":"…"}; запись —
{"r":[["поле", …]]}; признак — true/false. Прогонщик сверяет вход с
типами модели до вызова:
$ echo '{"fn":"Рассчитать скидку","args":[{"r":[["сумма",{"n":"-1"}],["постоянный клиент",true]]}]}' | python3 flang_cli.py prodazhi
{"ok":false,"code":"FLANG_TYPE","message":"вызов функции «Рассчитать скидку»: аргумент «покупка».сумма: -1 вне неотрицательное"}
Протокол один у всех целей. Та же модель, напечатанная в Java:
$ flang emit static/spec/models/order-discount.flang --target java --out printed-java
…
напечатано файлов 8, байт 163319, в printed-java
…
$ cd printed-java && make build
javac -encoding UTF-8 -Xlint:all -Werror -d . *.java
$ printf '{"fn":"Рассчитать скидку","args":[{"r":[["сумма",{"n":"20000"}],["постоянный клиент",true]]}]}\n' | java -cp . FlangCli Prodazhi
{"ok":true,"value":{"n":"3000"}}
Три тысячи — ровно то, что записано в примере «большая покупка постоянного клиента» в самой модели. Прогонщик нужен для вызова через трубу. В своей программе на Java функции зовутся напрямую:
Ctx ctx = Prodazhi.newContext();
Value покупка = Prodazhi.rec_pokupka(Value.number(20000), Value.flag(true));
System.out.println(Prodazhi.fn_rasschitat_skidku(ctx, покупка)); // <flang число: 3000>
Напечатанный код проверяет недоказанные обещания модели при каждом вызове и
бросает FLANG_PROPERTY, если обещание нарушено. Доказанные обещания в код не
едут.
Что выбрать
Способов три, и выбирают по одному вопросу — что вы готовы держать рядом со своим кодом.
| Способ | Что живёт рядом | Когда брать |
|---|---|---|
flang run в процессе |
двоичный flang и модель | редкие вызовы, скрипты, CI |
| HTTP-сервис | сервис с моделью, один на всех | высокая частота, короткий SLA |
| печать в целевой язык | ничего — только ваш код | flang поставить нельзя или процесс на вызов недопустим |
У третьего способа есть цена. Напечатанный код — срез модели на момент
печати: изменили модель — обязаны напечатать заново. Решается это шагом
--check в CI, который сравнивает напечатанное с моделью (глава
«Генерация и CI»). И ещё одно:
flang emit примеры не прогоняет, flang test нужен до печати.
Практика в песочнице
Найдите в отчёте о доказательствах три обещания, которые держатся только на
примерах. Именно из-за них flang run просит --trust. Поменяйте сумму в
примере «тот же расчёт без записи на входе» и посмотрите, какой пример
перестанет сходиться.
Типичные ошибки
Ниже — настоящие ответы flang run на модели из этой главы.
FLANG_TYPE, значение не того типа (код 1):
FLANG_TYPE: вызов функции «Скидка по сумме»: аргумент «сумма» не соответствует типу неотрицательное
FLANG_TYPE, аргумента не хватает (код 1, в примере пропущен
постоянный):
FLANG_TYPE: вызов функции «Скидка по сумме» функции «Скидка по сумме» не задаёт аргумент «постоянный»
FLANG_UNKNOWN_NAME, лишний аргумент (код 1). Лишний ключ flang не
пропускает молча: чаще всего это значит, что клиент и модель разошлись.
FLANG_UNKNOWN_NAME: вызов функции «Скидка по сумме» функции «Скидка по сумме» задаёт неизвестный аргумент «вес»
FLANG_UNKNOWN_NAME, опечатка в имени функции (код 1):
FLANG_UNKNOWN_NAME: не найдена функция «Расчитать скидку»
Запись в --args (код 2): --args принимает только плоский объект
скаляров.
flang run: «--args» разобрать не удалось — ждался плоский объект скаляров, вроде '{"н":10}'
Код 3 без вывода значения — программа не доказана, а --trust не передан.
Общее правило для клиента на любом языке: ветвитесь по коду FLANG_… и коду
возврата, а не по тексту сообщения. Текст пишется для человека.
Чек-лист
- Клиент читает stdout как значение только при коде возврата
0; при ненулевом — берёт причину из stderr. - Аргументы — плоский JSON скаляров в
--args; для записи в модели есть функция со скалярными аргументами. - Согласие на недоказанное (
--trust) передаётся явно, и в коде видно, что оно есть. - Ветвление по ошибке — по коду
FLANG_…, а не по тексту. - Для нечастых вызовов — процесс, для высокой частоты — HTTP-сервис, без
flang на машине — напечатанный модуль и шаг
--checkв CI.