К README · Указатель документации
Раскладка проекта на flang
Этот документ не список пожеланий. Каждое правило здесь выведено из работающего примера — docs/examples/library-api, предметной половины REST-сервиса библиотеки: выдача книг, каталог, штрафы за просрочку, — и на этот пример ссылается.
В примере лежит только половина службы на flang. Хозяина — HTTP-сервера и хранилища — в дереве нет, поэтому правила про то, что остаётся хозяину, сказаны без файла, на который можно указать; правила про модули на flang проверяются на дереве.
Пример устроен так:
docs/examples/library-api/
lib/ flang: чистые функции над данными проекта, включая предметные правила
api.flang модуль «Library API» — единственный вход, связывает остальные
catalog.flang модуль «Catalog» — отбор, сводка, карточки книг
fine.flang модуль «Fine» — тариф штрафа за просрочку
isbn.flang модуль «ISBN» — контрольная цифра, ключ книги
loan.flang модуль «Loan» — можно ли выдать книгу
query.flang модуль «Query» — разбор строки запроса «?a=1&b=2»
stdlib/ flang: библиотека проекта, про предметную область не знает
text.flang модуль «Project text» — функции над списком строк
Полная служба кладёт рядом с этими двумя каталог хозяина: HTTP-сервер (маршруты вроде GET /health, GET /books с отбором по автору и полке, POST /loans, POST /returns; percent-декодирование пути), хранилище и мост, который загружает lib/api.flang и вычисляет его функции. В этом примере такого слоя в дереве нет.
1. Три слоя и граница между ними
Правило. Логика уезжает в модуль flang, если у неё может быть пример. На хозяине остаётся то, у чего примера быть не может.
Почему. Пример — не документация, а исполняемая проверка: flang test находит их сам и падает, если поведение разошлось. Та же арифметика, написанная в обработчике HTTP, проверяется только тем тестом, который кто-то не поленится написать отдельно.
Где смотреть. Тариф штрафов, проверка контрольной цифры ISBN, разбор строки запроса и условие «кому можно выдавать книги» лежат в файлах docs/examples/library-api/lib/*.flang, проверяемых командой. У хозяина из этого не остаётся ничего.
Что остаётся хозяину и почему именно ему. Ввод-вывод в языке ОПИСЫВАЕТСЯ, но не выполняется: вариант «Прочитать файл» с путь равным … строит значение — поручение, — а исполняет его хозяин (docs/flang/SPEC.md, раздел «Ввод-вывод»). Поручений 27, набор закрыт, и соединения в нём есть — «Принять соединение», «Прочитать из соединения», «Ответить в соединение». Граница проходит не по ним, а по правилу этого раздела: у HTTP-сервера нет входа, на котором можно объявить пример. Он ждёт входящих, а не считает ответ по доводам, и проверять в нём примерами нечего. Поэтому сервер, маршрутизация и хранилище остаются хозяину — не потому, что язык их не выговаривает, а потому, что от переноса они не станут проверяемыми.
Правило не опирается на список того, чего в языке нет: функция — значение (снятие функций-значений по Рейнольдсу, docs/flang/SPEC.md, раздел 3), а поручения ввода-вывода в языке есть и исполняются командой flang io. Границу проводят примеры.
Проверить, что граница проведена верно, можно печатью. flang emit печатает библиотеку проекта целиком во все десять целевых языков — c, cpp, go, rust, python, java, csharp, elixir, js, ts, — а хозяин не печатается никуда, потому что печатать нечего. Если после переноса очередного куска в lib/ печать сломалась, кусок взят не тот.
Исключение, которое тоже правило. Не переписывай на flang то, что платформа уже делает правильно. Percent-декодирование — дело платформы хозяина (в JavaScript — decodeURIComponent), а не таблицы кодовых точек на flang: второй источник истины ради принципа хуже, чем один чужой.
2. Предметные правила и вычисления — разные модули
Правило. Модуль, в котором записаны тарифы, разрешения и формы данных, держится отдельно от модуля, который считает. Первый читает и правит не только программист, второй — только программист.
Почему. Их правят по разным поводам и с разной частотой. Тариф штрафа меняет библиотека, когда меняет тариф; контрольная цифра ISBN не меняется никогда. Сложи их в один файл — и правку тарифа придётся вносить в файл, который страшно трогать.
Где смотреть. lib/fine.flang — тариф штрафа и его потолок: три надбавки (50, 150 и 300 — в сумме ровно потолок), пять примеров и одно обеспечивает, читается без программиста. lib/isbn.flang — разложение строки на символы, свёртка с записью-аккумулятором, остаток от деления: тому, кто правит тариф, здесь делать нечего.
Потолок пишут обещанием, а не подрезкой. В fine.flang стоит обеспечивает «Штраф ограничен» результат не больше 500, и предел равен ровно сумме всех надбавок. Подрезать значение вместо обещания значило бы СКРЫТЬ выход за тариф; обещание же говорит обратное — выйти за 500 нельзя ни на одном входе.
Чего этим способом не получить. Право выдать книгу принимает обычная функция (lib/loan.flang), а не доказательство, построенное по снимку данных: ответ подкреплён только её телом и примерами. «Посчитано» — не «доказано», и страница говорит, что здесь именно первое.
3. Имена файлов
Правило. Имя файла — латиницей, строчными, слова через дефис. Расширение говорит, чем файл проверяется: .flang — языком, всё остальное — оснасткой того языка, на котором написан хозяин.
Почему латиницей. Имя файла едет в командные строки README, в образцы CI, в --out кодогенерации и в URL репозитория. Часть этих мест ломается на пробелах и кириллице молча — а молча ломающийся путь дороже красивого имени. Так уже устроен весь репозиторий: late-fee.flang стал бы fine.flang, credit-limit.flang, lists.flang.
Почему расширение важнее договорённости. Загрузчик flang выбирает разбор по суффиксу: .flang идёт через разборщик языка, .json — как готовый AST («Загрузить» в flang/self/link.flang). Файл с неверным расширением разберётся не тем разбором и даст непонятную ошибку.
Имя — про содержание, а не про слой. isbn.flang, а не isbn-module.flang: слой уже назван каталогом.
Когда иначе. Суффикс, который ищет инструмент, важнее правила: если прогонщик вашего хозяина ищет файлы образцом, имя подчиняется образцу, а не этому разделу. Документ проекта — README.md даже если написан по-русски: так велит правило именования документации в README.ru.md — README.md и SPEC.md рядом с кодом остаются под этими именами на том языке, на котором написаны, потому что GitHub показывает их титульной страницей каталога. Суффикс X.ru.md — для документов в docs/, где он отличает язык.
4. Имена внутри файлов
Правило. Имена функций, типов, полей и правил — по-русски. Имя из одного слова можно писать без кавычек, из нескольких — обязательно в ёлочках: «Рассчитать штраф», «дней просрочки».
Почему не латиницей. Поверхность языка русская целиком, и правило читает тот же человек, который читает его название. Смешение алфавитов в одной строке — лишняя работа глазу без единой выгоды. Английская поверхность у языка есть, но она отдельная и целиком — docs/examples/rosetta/factorial-english.flang рядом с factorial.flang, а не вперемешку в одном файле.
Почему ёлочки. Имя из нескольких слов иначе не отличить от продолжения конструкции; кавычки — не украшение, а граница токена. Обычные кавычки равноправны, ёлочки выбраны потому, что внутри строковых литералов стоят именно " — и глазу проще, когда имя и строка выглядят по-разному.
Форма имени постоянна. Язык намеренно не угадывает падежи (docs/site/language.ru.md), поэтому имя в вызове пишется так же, как в объявлении. Послабление есть только для локальных имён внутри функции flang и только по неизменяемой основе; на имена функций, типов и полей оно не распространяется.
Как называть.
| Что | Форма | Пример из проекта |
|---|---|---|
| функция-действие | глагольная фраза | «Рассчитать штраф», «Разобрать запрос», «Отобрать» |
| функция-предикат | утверждение | «Код верен», «Заявка принята» |
| тип и запись | существительное | «Книга», «Параметр», «Сводка» |
| поле записи | строчными, как в данных | «на полке», «дней просрочки» |
Имён вроде «Помощник», «Утилиты», «Общее» в проекте нет и быть не может: связывание сливает объявления в одну программу, и совпадение имён — ошибка FLANG_DUPLICATE_NAME, а не перекрытие (flang/self/link.flang). Имя обязано быть узнаваемым во всей собранной программе, а не только в своём файле.
Когда иначе. Наружу, в HTTP, уходят латинские имена: пути /books, /loans, /returns и параметры author, shelf. Их читает клиент, который про модель ничего не знает, и они — часть протокола, а не предметной области. Ключи JSON-тела при этом остаются русскими («код», «на полке»), потому что это поля модели: второй словарь имён пришлось бы держать в согласии вручную, а рассогласование двух словарей не ловит ни один тип.
5. Каталоги
Правило. Каталог — это слой, а не сущность. Не книги/ и выдачи/, а lib/, stdlib/ и — если хозяин у проекта есть — host/.
Почему. Слой отвечает на два вопроса сразу: кто это правит и чем это проверяется. lib/ и stdlib/ проверяются flang check|test; хозяин — обычный код, у которого этих проверок нет вовсе. Разложи то же самое по сущностям — и в одной папке окажутся файлы, проверяемые разными командами.
Проверка правила. Посмотри на файл и скажи, какой командой он проверяется. Не можешь ответить сразу — файл лежит не там.
6. Модули: где проходит граница
Правило. Один модуль — один файл. Первая строка — модуль «Имя», и это имя обязано совпасть с тем, как модуль импортируют.
Почему. Расхождение — ошибка FLANG_IMPORT_NAME (flang/self/link.flang): читатель видит одно имя, а получает объявления из другого файла.
Граница модуля — по вопросу «что меняется вместе». Правила отбора книг меняются вместе; формула контрольной суммы ISBN с ними не меняется никогда — поэтому lib/catalog.flang и lib/isbn.flang разные файлы.
Модуль не знает про своих потребителей. lib/query.flang не знает ни про книги, ни про ISBN: любой проект со строкой запроса взял бы его как есть. Знание про книги живёт этажом выше.
Граф импортов — дерево, слои сверху вниз. Цикл — ошибка FLANG_IMPORT_CYCLE, и это не ограничение, а подсказка: цикл означает, что граница проведена не там. В примере:
api.flang ──> catalog.flang ──> isbn.flang
│ └─> flang/stdlib/lists.flang (только «Минимум», «Все не меньше»,
│ «Сумма», «Максимум», «Длина»)
├─> query.flang ────> stdlib/text.flang (только «Первая часть», «Хвост через»,
│ «Непустые»)
├─> fine.flang
└─> loan.flang
Два имени в списке только у «Каталога» этот файл сам не зовёт — «Минимум» и «Все не меньше». Их зовёт КОНТРАКТ ввезённой «Суммы», а только сужает таблицу имён вместе с контрактами; без них модуль не собирался вовсе. Это тот случай, когда список только читается не из тела файла, а из отказа связывания.
У библиотеки проекта один входной модуль. lib/api.flang — единственный файл, который загружает хозяин; всё остальное подтягивает связывание. Причина в устройстве языка: импорт — это слияние объявлений, а не пространство имён, и загрузить два модуля по отдельности значит держать на хозяине две программы и помнить, какая функция в какой.
Во входном модуле не заводят своей предметной логики. Только то, что связывает нижние модули между собой. Входной модуль, в который начали дописывать «ещё одну функцию, ей же тут удобно», через полгода перестаёт быть входным.
7. Импорт: полный и выборочный
Правило. использует «Модуль» вносит все имена модуля. только «А», «Б» вносит перечисленные.
Главное про только: он перечисляет ИМЕНА, а не замыкание зависимостей. Взяв так функцию, которая внутри зовёт соседку по модулю, получишь FLANG_UNKNOWN_NAME на первом же обращении — и, если функция объявлена тотальной, ещё и FLANG_NOT_TOTAL вдогонку. Поэтому в примере «ISBN» берётся целиком («Код верен» зовёт «Цифры» и «Контрольную сумму»), а «Списки» и «Текст проекта» — выборочно: там все нужные функции самодостаточны.
Сам пример сегодня упирается ровно в это: flang check docs/examples/library-api/lib/api.flang отвечает «не проверено — замечаний 8», код 1. «Сумма», «Минимум», «Максимум» и «Все не меньше» из flang/stdlib/lists.flang зовут помощников «Шаг суммы», «Шаг минимума», «Шаг максимума», «Шаг все не меньше», которых в списке только у catalog.flang нет — FLANG_UNKNOWN_NAME и следом FLANG_NOT_TOTAL на каждую. Это ровно тот отказ, о котором абзац предупреждает; чинится он расширением списка только в catalog.flang, а не правкой этой страницы.
Помощник, не вписанный в только, не подменяется чужим тёзкой. Если ввезённая функция зовёт соседку по своему модулю, которой в списке нет, а одноимённую функцию приносит другой модуль или объявляет сам ввозящий файл, связывание отказывает тем же FLANG_DUPLICATE_NAME и называет оба файла и ввезённую функцию, которая зовёт тёзку. Имя внутри тела ввезённой функции значит то, что значило в её модуле, — иначе доказанная там гарантия в собранной программе стала бы ложью (ADR-0090). Место любой беды связывания называет файл: в файле …/b.flang: функция «Шаг» объявлена в двух модулях: ….
Зачем вообще сужать. Не ради чистоты: конфликт имён при связывании — ошибка, а не молчаливое перекрытие. Модуль на два десятка имён, внесённый целиком, — это два десятка будущих FLANG_DUPLICATE_NAME, и первое же совпадение остановит сборку.
Пути в строке нет — модуль ищется по имени. Имя — то, что стоит первой строкой файла в модуль «Имя». Смотрят по очереди: каталог самого файла, каждый каталог выше него (пока в каталоге есть файлы .flang) и библиотека, поставленная вместе с компилятором. Отсюда следствие, ради которого всё и сделано: файл можно переложить в другой каталог, и ни одна строка использует от этого не меняется.
Вглубь поиск не идёт. Модуль из соседней ветки дерева — например, stdlib/ проекта рядом с lib/ — либо называется путём (использует «Project text» из "../stdlib/text.flang"), либо его каталог называется один раз в FLANG_MODULE_DIR (каталоги через двоеточие). Путь, когда он написан, считается от каталога файла; абсолютный путь делает проект непереносимым, и в примере его нет ни разу.
Вверх поиск идёт, пока в очередном каталоге-предке лежит хоть один .flang, — то есть может выйти и за пределы проекта, и тогда чужой файл каталогом выше подменит модуль молча. Предел ставится переменной FLANG_MODULE_ROOT: она называет каталог, выше которого подъём не идёт. Не названа — подъём идёт, как описано выше.
8. Библиотека проекта (stdlib/)
Правило входа. Сюда попадает функция, которая не знает про предметную область, но нужна проекту.
Правило выхода наверх. Если функция пригодилась бы всякому, кто пишет на flang, её место не здесь, а в flang/stdlib — с примерами и разбором в общем наборе. Библиотека проекта — не свалка «пока не дошли руки».
Почему она вообще нужна. Параметрические типы в языке ЕСТЬ (тип «Возможно» от «А»), и часть flang/stdlib/lists.flang на них уже переведена: «Длина» от «А», «Приписать в начало» от «А», «Обратить» от «А» и ещё несколько работают со списком любых значений. Переведена не вся: «Элемент», «Индекс», «Без значения», «Минимум», «Сумма» объявлены для список числа и списку строк не годятся — у параметра типа нет ни порядка, ни арифметики, и граница проходит ровно по тому, сравнивает ли тело элементы. Списку строк — а из него состоит всё, что получается делением строки, — эти функции нужны заново, и писать их лучше один раз в одном месте.
Где смотреть. stdlib/text.flang — четыре функции над списком строк, ни одна не знает слова «книга».
9. Примеры
Правило. Пример — часть объявления, а не отдельный файл. пример пишется внутри самой функции.
Почему. flang test находит такие примеры сам и падает на расхождении. Пример, вынесенный в отдельный файл, никем не запускается, пока кто-нибудь не вспомнит.
Что закрывать примерами обязательно. Границу: пустой список, пустая строка, ноль, «не нашли». Такой пример есть у каждой функции проекта, у которой граница вообще бывает: «Пустой каталог», «Пустая строка», «Параметра нет», «Хвоста нет», «Вернули вовремя».
Пример на каждую ловушку, а не на каждую строку. «Разделитель внутри хвоста» в stdlib/text.flang существует потому, что склейка хвоста через = — ровно то место, где ошибаются: разрезав "поиск=a=b", вторым куском получаешь "a", а значением обязано быть "a=b".
Обещание пишут рядом с примерами, а не вместо них. обеспечивает держит все входы, пример — один; вместе они говорят разное, и одно другим не заменяется. У «Рассчитать штраф» стоит и то и другое.
Где живут функции без примеров. Их в проекте нет: у каждой функции, объявленной в lib/ и stdlib/, есть хотя бы один пример, и flang test проверяет это заодно с тотальностью. Точное число функций собранной программы здесь не названо намеренно: оно зависит от того, что втягивает связывание, и снимается прогоном, а не счётом в файлах.
10. Прогон и CI
Команды, которыми проверяется проект:
flang check docs/examples/library-api/lib/api.flang
flang test docs/examples/library-api/lib/api.flang
check входного модуля собирает всю программу; test прогоняет примеры всех собранных функций — своих и импортированных. Это причина, по которой достаточно назвать один файл: прибавили модуль — он приехал в проверку вместе со связыванием, без правки списка файлов.
Печать той же библиотеки в целевой язык — той же командой на тот же входной модуль: flang emit docs/examples/library-api/lib/api.flang --target js --out <каталог>. Печать отменяется, если программа не проходит проверку: emit смотрит то же, что check, и называет те же замечания.
Сегодня обе команды останавливаются на примере с кодом 1 и словами «не проверено — замечаний 8» (test добавляет «примеры не запускались — программа не прошла проверку»); причина и лечение названы в разделе 7.
Короткий список
- Может быть пример — уезжает в модуль языка; не может — остаётся у хозяина.
.flang— предметные правила и чистые вычисления; язык хозяина — внешний мир.- Имена файлов латиницей через дефис; расширение выбирает разбор.
- Имена внутри — по-русски, из нескольких слов — в ёлочках, форма постоянна.
- Каталог — слой, а не сущность; по файлу видно, какой командой он проверяется.
- Модуль — файл; граница — «что меняется вместе»; граф импортов — дерево.
- Один входной модуль на библиотеку, своей логики в нём нет.
толькоперечисляет имена вместе с их контрактами, а не зависимости; пути относительные.- В
stdlib/проекта — то, что не знает про предметную область. - Пример — часть объявления, а не отдельный файл.
- Потолок значения пишут обещанием
обеспечивает, а не подрезкой. - Одна команда на входной модуль проверяет всю программу:
checkсобирает,testпрогоняет.