Генератор произвольных списков в Power Query и Power BI
или “Как же все-таки работает List.Generate?”
List.Generate – одна из функций языка “M” (язык Power Query aka “Get & Transform” для Excel и редактора запросов Power BI), используемая для создания списков (lists ) по заданным правилам. В отличие от других генераторов списков (например, List.Repeat или List.Dates), правила и алгоритм генерации очередного элемента может быть практически любым, что позволяет использовать List.Generate для решения достаточно сложных задач.
Несмотря на то, что есть несколько отличных постов, описывающих использование этой функции (например, Chris Webb, Gil Raviv, PowerPivotPro, KenR), мне всегда не хватало более понятного описания – “как же это всё работает?!” или “Почему оно не работает?” и, наконец, “Что вообще имели ввиду разработчики?”
Как обычно, справка MSDN крайне лаконична:
Создает список значений с четырьмя функциями, которые создают начальное значение initial , проверяют выполнение условия condition и в случае успеха выбирают результат и формируют следующее значение next . Необязательный параметр selector может также быть указан.
Скажите, вы хотели бы использовать необязательный параметр selector ? Точно? Уверены?
Можно было бы списать на несовершенство перевода, но я вас уверяю, по-английски не лучше:
Generates a list of values given four functions that generate the initial value initial , test against a condition condition , and if successful select the result and generate the next value next . An optional parameter, selector , may also be specified.
Хотя на самом деле есть еще и краткое описание функции, которое выглядит чуть более внятно:
Создает список с указанием функции начального значения, функции условия, следующей функции и необязательной функции для преобразования значений.
По крайней мере, одно очевидно: функция принимает 4 аргумента, все 4 имеют тип function:
List.Generate(initial as function, condition as function, next as function, optional selector as nullable function) as list
На самом деле функция List.Generate использует достаточно простой алгоритм. В процессе генерации каждого элемента списка функция рассчитывает значение (назовем его CurrentValue), которое модифицируется и передается от одной функции-аргумента к другой в следующем цикле:
- Начальное значение CurrentValue = результат вычисления функции initial .
- Передать CurrentValue на вход функции condition , проверить условие и дать ответ true или false
- Если результат вычисления condition равен false – закончить создание списка.
- Если результат вычисления condition равен true – создать очередной элемент списка по следующему правилу:
- Если параметр selector задан, то вычислить значение функции selector, получив на вход CurrentValue.
- Если параметр selector отсутствует, очередной элемент создаваемого списка будет равен CurrentValue.
- Вычислить значение функции next (получив на вход CurrentValue) и присвоить переменной CurrentValue новое значение – результат этого вычисления.
- Перейти к шагу 2.
Как можно увидеть из этого описания, важным отличием List.Generate от других функций-итераторов языка M является то, что практически все остальные работают по принципу For Each…Next (то есть ограничены заданным списком перебора), в то время как List.Generate использует другую логику – Do While…Loop . Соответственно, количество элементов в созданном списке ограничивается только выполнением некоего условия.
Если записать приведенный выше алгоритм на каком-нибудь привычном (не функциональном) языке, то он мог бы выглядеть примерно так:
Dim NewList As New List CurrentValue = initial() Do While condition(CurrentValue) If selector = null Then NewList.Add(CurrentValue) Else NewList.Add(selector(CurrentValue)) End If CurrentValue = next(CurrentValue) Loop
При этом:
- Функция initial не имеет аргументов и ее вычисленное значение равно значению выражения, указанного внутри нее.
Если при написании initial вы укажете, что она должна принимать какие-то аргументы, у вас все равно не будет способа подать ей на вход значения, так как ее вызов происходит где-то внутри функции List.Generate.
Если честно, то я не понимаю, почему initial вообще задается как функция, а не сразу как вычисляемое ею значение. Наверное, для этого есть причины. - Сначала всегда вычисляется результат функции initial.
Если результат первой проверки condition будет false , то список не будет создан даже при том, что initial рассчитан.
Но если результат initial пройдет первую проверку, то именно он и будет первым элементом списка. Именно поэтому initial и next обычно генерируют одинаковые по типу и структуре значения. - Функции condition , next и selector получают на вход рассчитанное значение CurrentValue, но не обязаны его использовать. Фактически, эти три функции могут не обращать внимание на CurrentValue, и использовать какую-то другую логику.
Но, если честно, не представляю себе ситуацию, когда condition (или next ) не должна использовать CurrentValue, так как это приведет либо к бесконечному циклу, либо список не будет создан. - Функции next и selector вычисляются только при условии conditon = true .
- Функция selector вычисляется вне зависимости от результата функции next , рассчитанного на текущем шаге цикла.
- Функция next вызывается всегда ДО создания следующего элемента списка (2-го и далее).
Например, если вы создаете список в процессе работы с каким-либо API (например, в функциях initial и next посылаете запросы на подготовку или получение отчетов), имейте ввиду следующее:
- Как только List.Generate вызывается, API опрашивается как минимум 1 раз, при вычислении initial .
- Количество обращений к API всегда будет как минимум на 1 больше, чем количество элементов в созданном списке (этот лишний – последний результат расчета функции next , не прошедший проверку condition )
Удобнее всего, когда initial и next возвращают тип record . Это сильно упрощает добавление счётчиков и дополнительных аргументов для этих функций (одно из полей записи – основные данные, другое – счетчик, третье – какой-то еще параметр, используемый для генератора элементов, и так далее).
В итоге, List.Generate – это очень мощный по возможностям инструмент, хотя и немного тяжелый в освоении . Надеюсь, после этой статьи он стал понятнее и дружелюбнее. 🙂
Follow me: