Start der Reihe: Schleifen programmieren mit M in Power Query

Vielleicht bist Du ja eher in Programmiersprachen wie C#, Java oder Visual Basic zuhause. Sollte dem so sein, ist Dir die Programmstruktur der Schleife sicher bekannt. Eventuell ist Dir dieses Konzept sogar so in Fleisch und Blut übergegangen, dass Du es häufig ganz automatisch für die Lösung Deiner Aufgabenstellungen heranziehst. Es könnte sein, dass Du Dich sogar zum ersten Mal intensiv mit der M-Language beschäftigst und nun versuchst mit dieser Herangehensweise auch in Power Query zum Ziel zu kommen?

In M triffst Du dabei aber auf ein gänzlich anderes Programmierparadigma. Die M-Language ist eine funktionale Sprache, der die klassischen Schleifenkonstruktionen anderer Sprachen fremd sindAls grundlegendes Element für die Wiederholung von Ausdrücken greifen funktionale Programmiersprachen stattdessen auf das Mittel der Rekursion zurück und auch die M-Language bietet Dir diese Möglichkeit. Zudem verfügt M über weitere Funktionen, mit denen Du ein schleifenähnliches Verhalten nachstellen kannst.

Auch wenn solche Konstruktionen in funktionalen Sprachen nicht das erste, sondern eher eines der letzten Mittel der Wahl sein sollten, ist es gut zu wissen, dass sie Dir im Fall der Fälle auch in M zur Verfügung stehen. In dieser Artikelserie schauen wir uns an, wie das geht.

Im ersten Teil der Serie zeige ich Dir, wie Du rekursive Funktionen in M erzeugen kannst. Im zweiten Teil entwickeln wir dieses Konzept dann weiter und betrachten die Funktion List.Generate(). Im dritten und letzten Teil folgt dann die Verwendung der Funktion List.Accumulate().

Wie funktionieren rekursive Funktionen in M?

Rekursive Funktionen sind Funktionen, die sich innerhalb ihrer eigenen Funktionsdefinition wiederholt selbst aufrufen. Am besten wird das deutlich, wenn Du Dir das folgende einfache Beispiel anschaust:

Schleifen in M, Power Query, Power BI Desktop, List.Generate(), Lits.Accumulate, Scoping Operator, Rekursive Funktionen, M-language
Das Prinzip rekursiver Funktionen in M

Die nebenstehende Funktion "fnXmultiplizieren" erwartet als einzigen Parameter den Wert x als Zahl (grüner Teil). In unserem Beispiel gehen wir einfach mal davon aus, dass der Funktion der Wert 4 übergeben wird.

Diesen Wert überprüft die Funktion zunächst auf die Erfüllung eines beliebigen Kriteriums, in diesem kleinen Beispiel darauf, ob x größer als 150 ist (gelber Teil). Ist dieses Kriterium erfüllt, wird der aktuelle Wert von x ausgegeben.

Ist das Kriterium hingegen nicht erfüllt, wird der rote Teil der Funktion aufgerufen. In ihm erfolgt der erneute Aufruf der Funktion "fnXmultiplizieren". Dies geschieht, weil dem Funktionsnamen der sog. Scoping-Operator @ vorangestellt wir. Da die Funktion die Übergabe eines Parameters erwartet, wird ihr im Beispiel der Wert x multipliziert mit 3 übergeben. Dieser Prozess wiederholt sich nun, bis das Kriterium im gelben Teil erfüllt ist, sprich der beim wiederholten Aufruf übergebene Wert größer als 150 ist. Dann wird der rote Teil der Funktion nicht mehr ausgeführt und der aktuelle Wert von x ausgegeben. Im obigen Bild kannst Du sehen, dass dies nach vier Durchläufen passiert, wenn der Wert 324 erreicht ist.

Zu Erzeugung einer rekursiven Funktion in M benötigst Du also drei wesentliche Bestandteile:

  • Eine benutzerdefinierte M-Funktion, in der die Rekursion durchgeführt werden soll.
  • Ein Abbruchkriterium, häufig erzeugt über einen "if then else"-Ausdruck, der die Rekursion abbricht, sobald die gewünschte Bedingung erfüllt ist. So werden "Endlosschleifen" verhindert.
  • Den Scoping-Operator @, der der Funktion anzeigt, dass sie sich selbst aufrufen soll, solange das Abbruchkriterium nicht erfüllt ist.

Auf diese Weise kannst Du in M also Programmstrukturen erzeugen, die z.B. nach dem Prinzip von "Do-Loop"- bzw. "While-Wend"-Schleifen anderer Sprachen funktionieren.

 

Was geht noch mit rekursiven Funktionen? Natürlich kannst Du mit rekursiven Funktionen nicht einfach nur Zahlen hochzählen. Letztlich kannst Du in Deinen Funktionen sämtliche Standardfunktionen verwenden, die M anbietet. Entsprechend hast Du die Möglichkeit in rekursiven Funktionen alle Arten von Values, seien es Tables, Lists oder Records, zu bearbeiten bzw. zu behandeln.

Im Netz findest Du eine Reihe von Inspirationen für das Einsatzgebiet von rekursiven Funktionen. Ivan Bondarenko zeigt auf seinem Blog ein Beispiel zur Verarbeitung von Parent-Child Hierarchien mit Hilfe von rekursiven Funktionen. Ein weiteres Beispiel ist dieser Forumsbeitrag auf "Stack Overflow", in dem eine Tabelle automatisch um diverse Index-Spalten erweitert wird. 

Deiner Fantasie sind also kaum Grenzen gesetzt.

Warum funktionieren Rekursionen in M überhaupt?

Wenn Du ein wenig mit dem Environment-Konzept von M vertraut bist, fragst Du Dich vielleicht, warum rekursive Aufrufe innerhalb von Funktionen überhaupt funktionieren? Das Environment-Konzept von M trifft Aussagen zum Geltungsbereich von Values bzw. von benannten Values, auch Variablen genannt, innerhalb von M-Code. Im Grunde also, unter welchen Umständen Variablen in M aufeinander zugreifen können. Stark vereinfacht sagt es aus, dass Variablen nur auf Variablen zugreifen können, die sich in ihrem Environment bzw. im Environment ihres übergeordneten Ausdrucks befinden. Die Variable selbst ist dabei nicht in ihrem eigenen Environment enthalten, kann also nicht auf sich selbst zugreifen! Wenn diese Aussage stimmt, wie können rekursive Funktionen dann genau das tun?

Schleifen in M, Power Query, Power BI Desktop, List.Generate(), Lits.Accumulate, Scoping Operator, Rekursive Funktionen, M-language
Struktur eines Funktionsaufrufs in M

Grundsätzlich bleibt die vorangegangene Aussage natürlich bestehen. Wir müssen nur einen weiteren Sonderfall hinzufügen. Neben dem hier sehr kurz beschriebenen Environment einer Variablen erzeugen "Let"-Ausdrücke und Record-Initialisierungen noch ein zweites Environment, nämlich eines, in dem die initialisierte Variable selbst enthalten ist. Dies passiert also auch wenn wir, wie im linken Bild zu sehen, unsere kleine Beispielfunktion aufrufen. Für die Variable "Quelle", bzw. den darin enthaltenen Funktionsaufruf "fnXmultiplizieren", wird ein zusätzliches Environment erzeugt, in dem die Variable selbst vorhanden ist.

Variablen, Tabellen, Abfragen, Funktionen und andere Values werden in M über ihren Namen, ihren "Identifier", referenziert. Im Falle unserer Beispielfunktion also über den Identifier "fnXmultiplizieren". Bei einer Identifier-Referenz kann aber zwischen zwei Arten unterschieden werden:

Beim Namen "fnXmultiplizieren" handelt es sich um eine sog. "Exclusive-Identifier-Reference", also eine, in der die Funktion selbst exkludiert ist. Es gibt allerdings auch "Inclusive-Identifier-References". Diese ermöglichen den Zugriff auf die Version des Environments, in dem die initialisierte Variable selbst enthalten ist. 

Wie kannst Du nun "Exclusive-Identifier-References" in "Inclusive-Identifier-References" umwandeln? Ganz einfach, das geschieht eben durch das Voranstellen des Scoping-Operators @ vor den Identifier, also durch "@fnXmultiplizieren".

Was sind die Nachteile von Rekursiven Funktionen?

Wie Eingangs bereits angedeutet, empfehle ich Rekursionen in M erst als letzte Lösung für eine Problemstellung einzusetzen, wenn also keine Standardfunktion für die Lösung zur Verfügung steht. So leidet die Performance Deines Codes enorm darunter, wenn Du z.B. über alle Zeilen einer Tabelle einzeln iterierst, um die Zeilen für die Weiterverarbeitung anhand eines bestimmten Kriteriums zu identifizieren. Die Standardfunktion Table.SelectRows() wäre hierfür sicher die bessere Wahl. Dies ist nur eines von vielen denkbaren Beispielen und soll lediglich verdeutlichen, dass die Verarbeitung von ganzen Sets an Datenzeilen mit Hilfe von Standardfunktionen der einzelnen Behandlung von Zeilen vorgezogen werden sollte.

Sind rekursive Funktionen tatsächlich mal das Mittel der Wahl, solltest Du folgende Einschränkung beachten:

Rekursive Funktionen in M unterstützen keine "tail call elimination". Was ist das jetzt wieder? Wirst Du vielleicht fragen. Mir ging es zumindest so, als ich das erste Mal davon gehört habe:

Rekursive Funktionen in M und "tail call elimination"
Rekursive Funktionen in M und "tail call elimination"

Dieses Thema ist immer relevant, wenn die letzte Instruktion einer Funktion wieder der Aufruf einer Funktion ist, wie dies in unseren Beispielen der Fall ist. Vereinfacht gesagt, wird bei funktionalen Sprachen eine Funktion evaluiert, indem der Funktionsaufruf auf Deinem Computer in eine Art Warteschlange eingereiht wird. Diese Warteschlange wird "Call Stack" genannt und arbeitet die einzelnen Funktionsaufrufe ab, bevor er am Ende wieder geleert wird. Der "Call Stack" kann aber nicht beliebig viele Funktionsaufrufe in die Schlange aufnehmen. Irgendwann ist seine Kapazität ausgereizt.

Im obigen Bild habe ich beispielhaft die Funktion "fnFillCallStack" ausgeführt. Die Funktion ist ähnlich einfach wie unser Einführungsbeispiel. Sie erwartet zwei Parameter. Die Zahl x, die quasi den Startwert angibt und den Wert "Max", der die maximale Anzahl an Rekursionen enthält, die die Funktion durchlaufen soll. In jeder Schleife wird der Wert x einfach um 1 erhöht. Setze ich also x=1 und Max=1000 wird x solange um 1 erhöht, bis 1000 erreicht ist. 

In meinem Fall konnte ich beobachten, dass die Funktion ab ca. 11.400 Rekursionen mit dem oben abgebildeten Fehler beendet wurde, weil der "Call Stack" voll ist.

Programmiersprachen, die über "tail call elimination" verfügen, besetzen nicht für jeden Funktionsaufruf einen Platz im "Call Stack". Vielmehr wird beim erneuten Aufruf der Funktion der Platz des vorherigen Aufrufs wieder freigegeben und nur der aktuelle Aufruf in der Schlange gehalten. Auf diese Weise kann eine Funktion eine größere Rekursionstiefe aufweisen und besetzt immer nur einen konstanten Platz im "Call Stack".

Neben vergleichsweise schlechter Performance kann die Verwendung von rekursiven Funktionen in M also sogar zum Scheitern Deines kompletten Datenverarbeitungsprozesses führen.

Was haben wir gelernt und wie geht es weiter?

Die Erzeugung von Schleifenkonstrukten in M kann mit Hilfe von rekursiven Funktionen auf relativ einfach Art erreicht werden. Hierzu benötigst Du lediglich eine Funktion, ein Abbruchkriterium für die Funktion und den Scoping-Operator @.

Beachten solltest Du dabei, Rekursionen in M nicht leichtfertig einzusetzen, weil mit ihrer Verwendung nachteilige Auswirkungen auf die Performance bis hin zum kompletten Scheitern Deines Datenverarbeitungsprozesses einhergehen können.

Die beschriebenen Nachteile von rekursiven Funktionen in M kannst Du mit den Standardfunktionen List.Generate() bzw. List.Accumulate() überwinden und solltest diese deshalb bevorzugt verwenden. Wie das geht, zeige ich Dir in den weiteren Teilen zu dieser Artikelserie.

Kennst Du schon die anderen Teile der Serie?

Ich hoffe der Artikel hat Dir gefallen und Du bist auch bei den weiteren Teilen der Serie dabei. Wenn dem so ist, dann teile den Artikel doch einfach mit anderen!

 

Bis zum nächsten Mal!

 

Viele Grüße aus Hamburg

 

Uwe

BESTENS INFORMIERT
Entwickle Dein Know-How und Deine Möglichkeiten im Reporting
und erhalte mit dem Newsletter exklusive Beispieldateien
zu unseren Artikeln

DURCHSUCHE data-insights.de...

RSS-FEED BI BLOG...


Kommentar schreiben

Kommentare: 0