Функция Expression.Evaluate() в Power Query/M
Текст представляет собой адаптированный перевод статьи Chris Webb (Крис Вебб),
оригинал – Expression.Evaluate() In Power Query/M. Рассматривается англоязычный Power Query.
Крис Вебб (Chris Webb) — независимый эксперт, консультант по технологиям Analysis Services, MDX, Power Pivot, DAX, Power Query и Power BI. Его блог — это кладезь информации на тему перечисленных технологий. Вот уже более 10 лет он пишет про BI-решения от Microsoft. Количество его статей перевалило за 1000! Также Крис выступает на большом количестве различных конференций вроде SQLBits, PASS Summit, PASS BA Conference, SQL Saturdays и участвует в различных сообществах.
Крис любезно разрешил нам переводить его статьи на русский язык. И это одна из них.

Функция Expression.Evaluate() в Power Query/M

Некоторое время назад была написана статья, где описывается загрузка М кода из текстового файла с помощью функции Expression.Evaluate(). Сегодня мы рассмотрим эту функцию более подробно и добавим несколько примеров к тем, что есть на официальном сайте Майкрософт.

Документация даёт ясный ответ, что делает Expression.Evaluate: берёт выражение на языке М, вычисляет и возвращает результат. Тут важно понимать, что выражение на языке М может быть больше, чем строка кода. Фактически, запрос Power Query является одним выражением.

Вот простой пример использования Expression.Evaluate():
let
    Source = Expression.Evaluate("""Hello World""")
in
    Source
В результате получаем текст «Hello World»:
А вот другой пример, возвращающий число 10:
let
    Source = Expression.Evaluate("6+4")
in
    Source
Помните, что запрос Power Query это одно выражение? Достигается это через использование оператора let. В результате одно выражение может состоять из многих подвыражений.

Вот пример с оператором let, возвращающий 12:
let
    Source = Expression.Evaluate("let a=3, b=4, c=a*b in c")
in
    Source
Хорошо, пока не очень интересно. Что нам действительно надо, это вычисление сложных выражений.

Рассмотрим следующий запрос, который использует функцию Text.Upper() и возвращает текст «ABC»:
let
    Source = Text.Upper("abc")
in
    Source
Но если мы попытаемся выполнить следующий запрос, то получим ошибку: Имя «Text.Upper» не существует в текущем контексте.
let
    Source = Expression.Evaluate("Text.Upper(""abc"")")
in
    Source
Для полного понимания того, что происходит, мы рекомендуем ознакомиться с разделом «3.3 Environments and Variables» спецификации языка. Краткое объяснение в том, что все выражения вычисляются в «среде окружения», где существуют другие переменные, на которые можно ссылаться.
Причина, по которой запрос выдал ошибку в том, что вычисления происходят в его собственной среде, где глобальная библиотека функций недоступна. Чтобы это исправить, подключим глобальную среду, указав вторым параметром предопределённую переменную #shared:
let
    Source = Expression.Evaluate("Text.Upper(""abc"")", #shared)
in
    Source
#shared возвращает запись, содержащую имена всех переменных в области глобальной среды окружения. Это можно использовать в запросе для получения всех доступных переменных (включая функции глобальной библиотеки и других запросов текущей книги:
let
    Source = #shared
in
    Source
Вот пример того, что возвращает данный запрос из книги, содержащей несколько запросов:
Применение #shared позволяет вычислять выражения, использующие глобальную библиотеку функций. Но это не волшебная палочка, убирающая все ошибки. В следующем запросе объявляется список и делается попытка получить второй элемент с помощью Expression.Evaluate():
let
    MyList = {1,2,3,4,5},
    GetSecondNumber = Expression.Evaluate("MyList{1}", #shared)
in
    GetSecondNumber
Несмотря на использование #shared во втором параметре, мы получили ошибку, т.к. переменная MyList остаётся недоступной. Для исправления этого, определим вторым параметром Expression.Evaluate() запись, чтобы среда окружения знала на что ссылается переменная MyList:
let
    MyList = {1,2,3,4,5},
    GetSecondNumber = Expression.Evaluate("MyList{1}", [MyList=MyList])
in
    GetSecondNumber
Вот пример посложнее. Надеемся, он поможет лучше понять логику построения запроса:
let
    MyList_Outer = {1,2,3,4,5},
    NumberToGet_Outer = 3,
    GetNthNumber = Expression.Evaluate("MyList_Inner{NumberToGet_Inner}",
       [MyList_Inner=MyList_Outer, NumberToGet_Inner=NumberToGet_Outer ])
in
    GetNthNumber
В этом примере мы видим, что две переменных, переданных в тексте первого параметра Expression.Evaluate() присутствуют во втором параметре, где в записи им сопоставляются соответствующие переменные главного запроса. Итак, как же нам одновременно передать свои переменные и использовать функции глобальной библиотеки?

Нам нужно создать запись, содержащую все имена #shared плюс те, что мы хотим передать дополнительно. В этом нам поможет Record.Combine(). В результате мы получим объединённую запись нашей собственной записи и записи #shared:
let
    MyList_Outer = {1,2,3,4,5},
    NumberToGet_Outer = 1,
    RecordOfVariables =
     [MyList_Inner=MyList_Outer, NumberToGet_Inner=NumberToGet_Outer ],
    RecordOfVariablesAndGlobals = Record.Combine({RecordOfVariables, #shared}),
    GetNthNumber = Expression.Evaluate(
     "List.Reverse(MyList_Inner){NumberToGet_Inner}",
     RecordOfVariablesAndGlobals )
in
    GetNthNumber