Lambda Expressions in Power Query - Teil 3 - List.Generate

Von Lukas Hillesheim, 30. August 2022
Blog Article & Copyright by Lukas Hillesheim
Artikel 3 dieser Serie behandelt "List.Generate". Es handelt sich hierbei um eine der wichtigsten Power Query Funktionen. So wie in anderen Sprachen verkapselt "List.Generate" eine Schleife und stellt das Pendant eines ForEach-Loops dar. Für das Verständnis von "List.Generate" sind Kenntnisse über Lambda Expressions notwendig.

1. “List.Generate” vs. “For-Loop”

Die MSDN Syntax-Hilfe für "List.Generate":
  • List.Generate(
  • initial as function,
  • condition as function,
  • next as function,
  • optional selector as nullable function) as list
For-Loop:
  • For ( <variable_expression>; <evaluate_expression>; <increment_expression> ) {
  • <loop_block_expression>
  • }
Die meisten For-Loops sehen so oder ähnlich aus:
  • For ( Int i = 0; i <= 5; i++ ) {
  • <loop_block_expression>
  • }
Vergleich “List.Generate” / “For-Loop”:
Figure 1 - Comparison List.Generate and For-Loop
Figure 1 - Comparison List.Generate and For-Loop

2. Allgemeines

Beispiel 1 - Generieren einer Liste mit 5 Zahlen
Figure 3 - "Each"-syntax
Figure 2 - "Each"-syntax
Output:
Figure 3 - List with 5 numbers
Figure 3 - List with 5 numbers
In Artikel 1 dieser Serie habe ich empfohlen, die "Each"-Syntax nicht zu verwenden und statt dessen die native Lambda Expressions Syntax zu benutzen. Dieser Ratschlag gilt für den Fall, dass Sie noch am Anfang mit Power Query stehen.
Jede der vier Argumente von "List.Generate" ist vom Typ "function" und daher ist es syntaktisch erlaubt, native Lambda Expressions einzusetzen.
Beispiel 1 umformuliert. Verwendung einer nativen Lambda Expression:
Figure 4 – Native lambda syntax
Figure 4 – Native lambda syntax
Die drei Lambda Expressions sind voneinander unabhängig. Daher ist es erlaubt, verschiedene Variablen-Namen für jede einzelne Lambda Expression zu benutzen:
Figure 5 - Different variable names
Figure 5 - Different variable names

3. Loop Block / Selector

Das vierte Argument, der Selector, ist optional und die meisten Beispiele aus dem Internet wie auch die vorangegangenen Beispiele lassen es weg. Wenn Sie das "Selector"-Argument nicht verwenden, ist das Default-Verhalten von PowerQuery so, dass das "Selector"-Argument mit dem "Next"-Argument identisch ist.
Im nächsten Code-Beispiel wird das implizite Verhalten dargestellt:
Figure 6 – ‘Next’-argument explicitly duplicated as ‘Selector’-argument
Figure 6 – ‘Next’-argument explicitly duplicated as ‘Selector’-argument
Die Vergleichs-Matrix oben zeigt, dass das "Selector"-Argument von "List.Generate" der <loop_block_expression> in einem "For-Loop" entspricht. Oder in anderen Worten: mit Hilfe des "Selector"-Arguments können Sie steuern, was pro Iteration in das Result-Set eingefügt wird. Im Beispiel von oben ist der Wert, der jeweils ins Result Set eingefügt wird, der Schleifen-Zähler selbst.
Ein "For-Loop" würde nach dieser Logik folgendermaßen aussehen:
  • Int[] result = {};
  • For ( int i = 1; i <= 5; i++ ) {
  • result += i;
  • }
Beispiel 2 - Verwenden des Selectors.
Figure 7 - Selector with a constant value
Figure 7 - Selector with a constant value
Im Beispiel wird Argument 4, der Selector, verwendet, um pro Schleifendurchgang den konstanten Wert "test" zurückzugeben. Die Argumente 1 bis 3 sorgen dafür, dass die Schleife 5 Mal ausgeführt wird.
Figure 8 - Selector with a constant value. Result.
Figure 8 - Selector with a constant value. Result.

4. Loop-Header / Initial- & Condition- & Next-Lambda Expressions

Beispiel 3 - Übergeben eines Record-Objekts.
Figure 9 - Complex sample. Code.
Figure 9 - Complex sample. Code.
- Schleifen-Zähler. In Beispiel 3 wird die Schleife 5 Mal durchlaufen. Der Loop Counter startet mit 1 und wird jeweils um 1 inkrementiert.
- OrderDate. Der Loop Counter wird als Offset zwischen dem konstanten Datum "1.3.2022" und dem OrderDate verwendet.
- Customer. Der Loop Counter wird für die Berechnung eines Buchstabens verwendet.
- Amount. Ein Random Value zwischen 30 und 60.
Figure 10 - Complex example. Pass a record object.
Figure 10 - Complex example. Pass a record object.
Initial Lambda Expression. Das erste Argument, die "Initial" Lambda Expression gibt in diesem Beispiel einen Record zurück. Der Record wird als erstes Element in das Ergebnis eingefügt. Bitte beachten Sie, dass "List.Generate" an die "Initial" Lambda Expression keine Daten übergibt. Daher sind die Klammern leer; jede Variable innerhalb der Klammern würde eine Ausnahme verursachen. Den Fall eines fehlenden vierten Arguments kann man sich im OOP-Vergleich folgendermaßen vorstellen:
  • Record[] result = {};
  • For ( Record r = […], … ) { … }
In Power Query ergibt sich durch die interne Ablauf-Logik von "List.Generate", dass der Wert für die "Initial" Lambda Expression auf jeden Fall in das Result Set eingefügt, bevor "Next" Lambda Expression und "Condition" Lambda Expression Einfluss auf den Verlauf der Schleife nehmen. Im Sinne von VB, C# etc. handelt es sich hierbei also um eine "fussgesteuerte Schleife" (Do-While). Dies ist ein wichtiger konzeptioneller Aspekt, denn da die Steuerung der Schleife erst am Ende stattfindet, enthält das Result Set mindestens ein Element, sei es leer oder nicht-leer.
  • result += r;
Condition Lambda Expression. Die "Condition Lambda Expression" wird ausgewertet, wenn der nächste Schleifendurchlauf startet. Um die Schleife 5 Mal laufen zu lassen, wird ein Upper Bound von 5 definiert und der aktuelle Wert jeweils mit Upper Bound verglichen. Die "Next" Lambda Expression ist dafür verantwortlich, die "record" Variable zu setzen, und vergleicht die aktuelle Id mit dem Upper Bound - in diesem Beispiel mit dem Wert 5. Auf die aktuelle Id wird mit folgendem Ausdruck zugegriffen: "record[Id]".
Next Lambda Expression. Die "Next Lambda Expression" greift auf die Variable "record" zu. Die "record" Variable erhält ihren Wert von der äußeren Funktion "List.Generate". "record" repräsentiert das letzte Element, das in das Result Set eingefügt wurde. Wenn der Loop zum ersten Mal startet, stellt die "Initial" Lambda Expression den Wert zur Verfügung. Oder anders ausgedrückt ist das letzte eingefügte Element im Falle des Schleifenstarts der Rückgabewert der "Initial" Lambda Expression. Die Variable der linken Seite wird auf der rechten Seite aufgerufen und die Lambda Expression generiert daraus neue Daten. Insbesondere wird eine neue Id erzeugt. Weil die "Selector" Lambda Expression fehlt, wird der Rückgabewert der "Next" Lambda Expression verwendet, um den aktuellen Record in das Result Set einzufügen.

5. Alternatives Pattern / Imperativer Stil

Der Code oben ist nicht so einfach zu lesen und auch nicht einfach zu verstehen! Für einen Entwickler kann es verwirrend sein, dass - wenn die "Selector Lambda Expression" fehlt - die "Selector Lambda Expression" gleichgesetzt wird mit der "Next Lambda Expression". Das nächste Code-Beispiel ist die Projektion einer PowerQuery-Schleife mit List.Generate ohne das vierte Argument. Man sieht, dass die automatische Verhaltensweise dazu führt, dass die Schleifen-Variable ins Ergebnis eingefügt wird:
  • Int[] result = {};
  • For ( int i = 0; i <= 5; i++ ) {
  • result += i;
  • }
Unter Verwendung der "Selector Lambda Expression" kann der Code in ein mehr imperatives Pattern umgeschrieben werden. Dadurch wird der Code besser verständlich:
Figure 11 - Alternate pattern / imperative style.
Figure 11 - Alternate pattern / imperative style.
Figure 12 - Alternate pattern / imperative style. Result.
Figure 12 - Alternate pattern / imperative style. Result.
Nach dem Rewrite wird der Unterschied zwischen Elementen der Schleifen-Steuerung und dem (Schleifen-) Block deutlicher. Die ersten drei Lambda Expressions kümmern sich um Loop-Initialisierung, Loop-Counter und Counter Inkrement. Die vierte Lambda Expression verwendet den Loop Counter, um den Ergebnis-Ausdruck zu parametrieren. Die Loop Counter Variable ist leicht zu verstehen, weil es sich hierbei - wie bei Schleifen üblich - um einen Integer handelt, der jeweils um 1 inkrementiert wird. Im Gegensatz zum ursprünglichen Pattern muss der Entwickler nicht die "record" Variable verstehen, deren Inhalt (intern) hin und her gereicht wird.
Beachten Sie auch, dass jetzt, wo die "Selector Lambda Expression" präsent ist, die "Initial Lambda Expression" NICHT (!) das erste Element für das Result Set liefert, sondern die "Selector Lambda Expression"!