Функция M: один контракт для сотни файлов
Функция нужна не тогда, когда код выглядит умно, а когда одно и то же правило должно честно применяться к разным входам.
В папке лежат книги филиалов. В каждой нужен лист Data, четыре обязательных столбца и код филиала из имени файла. Можно скопировать запрос десять раз, но любое исправление придётся повторять. Функция M превращает это правило в контракт: получает бинарный файл и его имя, возвращает типизированную таблицу или объясняет, почему не может.
Сигнатура рассказывает половину истории
(content as binary, fileName as text) as table =>
let
Workbook = Excel.Workbook(content, null, true),
DataRows = Table.SelectRows(
Workbook,
each [Kind] = "Sheet" and [Item] = "Data"
),
Sheet =
if Table.RowCount(DataRows) = 1
then DataRows{0}[Data]
else error Error.Record(
"MissingSheet",
"В книге нет единственного листа Data",
[File = fileName]
),
Headers = Table.PromoteHeaders(Sheet, [PromoteAllScalars=true]),
Required = {"OrderID", "OrderDate", "SKU", "Amount"},
Missing = List.Difference(Required, Table.ColumnNames(Headers)),
Checked =
if List.IsEmpty(Missing)
then Headers
else error Error.Record(
"MissingColumns",
"В файле отсутствуют обязательные столбцы",
[File = fileName, Columns = Missing]
),
Typed = Table.TransformColumnTypes(
Checked,
{{"OrderID", type text}, {"OrderDate", type date},
{"SKU", type text}, {"Amount", Currency.Type}},
"ru-RU"
),
WithSource = Table.AddColumn(Typed, "SourceFile", each fileName, type text)
in
WithSource
Явные типы параметров ловят неправильный вызов раньше. Результат as table обещает вызывающему запросу таблицу, а не иногда список, иногда null. M вычисляет выражения по зависимостям, поэтому названия шагов важнее их визуального порядка: Typed ссылается на Checked и не может возникнуть раньше.
Ошибка тоже является результатом контракта
Обращение DataRows{0} без проверки породило бы техническую ошибку «ключ не соответствует строкам таблицы». Она ничего не говорит владельцу файла. Мы сначала проверили ровно один лист и создали Error.Record с кодом, сообщением и именем. Вызывающий запрос может применить try fxReadSales(...) и собрать ошибки по файлам.
Не следует превращать функцию в универсальный комбайн с двадцатью логическими параметрами. Если два источника имеют разную семантику, им нужны две небольшие функции и, возможно, общий вспомогательный шаг. Параметр уместен, когда меняется значение, а не весь алгоритм.
Проверяем функцию без папки
Создайте три крошечных файла: корректный, без Amount и с двумя листами Data. В отдельных запросах вызовите функцию напрямую через File.Contents. Такой набор быстрее полного обновления папки и остаётся регрессионной проверкой после изменения функции.
Проверка должна охватывать типы. Текст «неизвестно» в Amount обязан стать видимой ошибкой, а не нулём. Дата 03/04/2026 должна читаться по договорённой локали, иначе апрель и март поменяются местами без ошибки.
Метаданные функции для коллеги
В Power Query можно добавить описание функции и параметров через тип с метаданными. Даже без этого оставьте рядом запрос-пример с одним вызовом и таблицей ожидаемых столбцов. Пользователь функции не должен открывать Advanced Editor, чтобы понять, что передавать.
Критерий готовности: новый разработчик получает функцию, три тестовых файла и описание результата. Он должен суметь добавить ещё один филиал, не меняя тело функции и не спрашивая автора о скрытых предположениях.
Функция M ценна не сокращением строк, а централизацией договора. Один вход, один результат, понятные отказы — и изменение формата исправляется в одном месте.