Freigeben über


Grundlegende Konzepte

In diesem Abschnitt werden die grundlegenden Konzepte erläutert, die in den folgenden Abschnitten behandelt werden.

Werte

Ein einzelnes Datenelement wird als Wert bezeichnet. Vereinfacht gesagt gibt es zwei allgemeine Kategorien von Werten: primitive Werte, die nicht teilbar sind, und strukturierte Werte, die aus primitiven Werten und anderen strukturierten Werten erstellt werden. Beispielsweise handelt es sich bei den Werten

1 
true
3.14159 
"abc"

um primitive Werte, da sie nicht aus anderen Werten bestehen. Andererseits werden die Werte

{1, 2, 3} 
[ A = {1}, B = {2}, C = {3} ]

mithilfe primitiver Werte und im Fall des Datensatzes anderer strukturierter Werte erstellt.

Ausdrücke

Ein Ausdruck ist eine Formel, die zum Erstellen von Werten verwendet wird. Ein Ausdruck kann mit einer Vielzahl syntaktischer Konstrukte gebildet werden. Nachfolgend finden Sie Beispiele für Ausdrücke. Jede Zeile ist ein separater Ausdruck.

"Hello World"             // a text value 
123                       // a number 
1 + 2                     // sum of two numbers 
{1, 2, 3}                 // a list of three numbers 
[ x = 1, y = 2 + 3 ]      // a record containing two fields: 
                          //        x and y 
(x, y) => x + y           // a function that computes a sum 
if 2 > 1 then 2 else 1    // a conditional expression 
let x = 1 + 1  in x * 2   // a let expression 
error "A"                 // error with message "A"

Die einfachste Form des Ausdrucks ist wie oben gezeigt ein Literal, das einen Wert darstellt.

Komplexere Ausdrücke werden aus anderen Ausdrücken erstellt, die als Unterausdrücke bezeichnet werden. Beispiel:

1 + 2

Der obige Ausdruck besteht aus drei Ausdrücken. Die Literale 1 und 2 sind Unterausdrücke des übergeordneten Ausdrucks 1 + 2.

Das Ausführen des Algorithmus, der durch die syntaktischen Konstrukte definiert ist, die in einem Ausdruck verwendet werden, wird als Auswertung des Ausdrucks bezeichnet. Jede Art von Ausdruck hat Regeln für die Auswertung. Beispielsweise erzeugt ein Literalausdruck wie 1 einen konstanten Wert, während der Ausdruck a + b die resultierenden Werte übernimmt, die durch Auswerten zweier anderer Ausdrücke (a und b) erzeugt werden, und diese nach einem Regelsatz hinzufügt.

Umgebungen und Variablen

Ausdrücke werden in einer bestimmten Umgebung ausgewertet. Bei einer Umgebung handelt es sich um eine Reihe benannter Werte, die Variablen genannt werden. Jede Variable in einer Umgebung hat innerhalb der Umgebung einen eindeutigen Namen, der als Bezeichner bezeichnet wird.

Ein Ausdruck der obersten Ebene (oder Stammebene) wird innerhalb der globalen Umgebung ausgewertet. Die globale Umgebung wird von der Ausdrucksauswertung bereitgestellt, anstatt durch den Inhalt des auszuwertenden Ausdrucks bestimmt zu werden. Der Inhalt der globalen Umgebung umfasst die Standardbibliotheksdefinitionen und kann von Exporten aus Abschnitten mehrerer Dokumente beeinflusst werden. (Aus Gründen der Einfachheit gehen die Beispiele in diesem Abschnitt von einer leeren globalen Umgebung aus. Das heißt, es wird angenommen, dass keine Standardbibliothek vorhanden ist und keine anderen abschnittbasierten Definitionen vorhanden sind.)

Die Umgebung, die zum Auswerten eines Unterausdrucks verwendet wird, wird durch den übergeordneten Ausdruck bestimmt. Die meisten übergeordneten Ausdrucksarten werten einen Unterausdruck in derselben Umgebung aus, in der sie ausgewertet wurden, einige verwenden dabei aber eine andere Umgebung. Die globale Umgebung ist die übergeordnete Umgebung, in der der globale Ausdruck ausgewertet wird.

Beispielsweise wird mit dem Datensatzinitialisierungsausdruck der Unterausdruck für jedes Feld mit einer geänderten Umgebung ausgewertet. Die geänderte Umgebung enthält mit Ausnahme des zu initialisierenden Felds eine Variable für jedes der Datensatzfelder. Wenn Sie die anderen Felder des Datensatzes einschließen, können die Felder von den Werten der Felder abhängen. Beispiel:

[  
    x = 1,          // environment: y, z 
    y = 2,          // environment: x, z 
    z = x + y       // environment: x, y
] 

Ebenso wertet der let-Ausdruck den Unterausdruck für jede Variable mit einer Umgebung aus, die mit Ausnahme der zu initialisierenden Variable jede der Variablen enthält. Der let-Ausdruck wertet den Ausdruck nach „in“ mit einer Umgebung aus, die alle Variablen enthält:

let 

    x = 1,          // environment: y, z 
    y = 2,          // environment: x, z 
    z = x + y       // environment: x, y
in
    x + y + z       // environment: x, y, z

(Es stellt sich heraus, dass sowohl der Datensatzinitialisierungsausdruck als auch der Let-Ausdruck tatsächlich zwei Umgebungen definieren, von denen eine die Variable enthält, die initialisiert wird. Dies ist nützlich für erweiterte rekursive Definitionen und wird unter Bezeichnerverweise behandelt.)

Die neuen Variablen werden mit den Variablen in der übergeordneten Umgebung zusammengeführt, um die Umgebungen für die Unterausdrücke zu bilden. Das folgende Beispiel zeigt die Umgebungen für geschachtelte Datensätze:

[
    a = 
    [ 

        x = 1,      // environment: b, y, z 
        y = 2,      // environment: b, x, z 
        z = x + y   // environment: b, x, y 
    ], 
    b = 3           // environment: a
]  

Das folgende Beispiel zeigt die Umgebungen für einen geschachtelten Datensatz in einem let-Ausdruck:

Let
    a =
    [
        x = 1,       // environment: b, y, z 
        y = 2,       // environment: b, x, z 
        z = x + y    // environment: b, x, y 
    ], 
    b = 3            // environment: a 
in 
    a[z] + b         // environment: a, b

Das Zusammenführen von Variablen mit einer Umgebung führt möglicherweise zu einem Konflikt zwischen den Variablen (da jede Variable in einer Umgebung einen eindeutigen Namen haben muss). Der Konflikt wird wie folgt gelöst: Wenn der Name einer neuen Variablen, die zusammengeführt wird, mit dem einer vorhandenen Variablen in der übergeordneten Umgebung identisch ist, hat die neue Variable in der neuen Umgebung Vorrang. Im folgenden Beispiel hat die innere (tiefer geschachtelte) Variable x Vorrang vor der äußeren Variablen x.

[
    a =
    [ 
        x = 1,       // environment: b, x (outer), y, z 
        y = 2,       // environment: b, x (inner), z 
        z = x + y    // environment: b, x (inner), y 
    ], 
    b = 3,           // environment: a, x (outer) 
    x = 4            // environment: a, b
]  

Bezeichnerverweise

Ein Bezeichnerverweis wird verwendet, um auf eine Variable in einer Umgebung zu verweisen.

identifier-expression:
      identifier-reference
identifier-reference:
      exclusive-identifier-reference
      inclusive-identifier-reference

Die einfachste Form des Bezeichnerverweises ist ein Exklusivbezeichnerverweis:

exclusive-identifier-reference:
      identifier

Es ist ein Fehler, wenn eine exklusive Bezeichner-Referenz auf eine Variable verweist, die nicht Teil der Umgebung des Ausdrucks ist, in dem der Bezeichner erscheint.

Es ist ein Fehler, wenn eine Referenz auf eine exklusive Kennung auf eine Kennung verweist, die gerade initialisiert wird, wenn die Kennung, auf die verwiesen wird, in einem record-initializer-expression oder let-expression definiert ist. Stattdessen kann eine Inclusive-Identifier-Referenz verwendet werden, um Zugriff auf die Umgebung zu erhalten, die den zu initialisierenden Identifier enthält. Wenn in einer anderen Situation eine inklusive Kennungsreferenz verwendet wird, entspricht sie einer exklusiven Kennungsreferenz.

Inklusivbezeichnerverweis:
      @ Bezeichner

Dies ist nützlich, wenn rekursive Funktionen definiert werden, da der Name der Funktion normalerweise nicht im Gültigkeitsbereich liegt.

[ 
    Factorial = (n) =>
        if n <= 1 then
            1
        else
            n * @Factorial(n - 1),  // @ is scoping operator

    x = Factorial(5) 
]

Wie bei einem Datensatzinitialisierungsausdruck kann ein Inklusivbezeichnerverweis innerhalb eines let-Ausdrucks für den Zugriff auf die Umgebung verwendet werden, die den zu initialisierenden Bezeichner enthält.

Reihenfolge der Auswertung

Beachten Sie den folgenden Ausdruck, der einen Datensatz initialisiert:

[ 
    C = A + B, 
    A = 1 + 1, 
    B = 2 + 2 
]

Bei der Auswertung erzeugt dieser Ausdruck den folgenden Datensatzwert:

[ 
    C = 6, 
    A = 2, 
    B = 4 
]

Der Ausdruck gibt an, dass die Werte der Felder A und B bekannt sein müssen, um A + B für Feld C berechnen zu können. Dies ist ein Beispiel für eine Abhängigkeitsreihenfolge von Berechnungen, die von einem Ausdruck bereitgestellt werden. Die M-Auswertung hält sich an die von den Ausdrücken bereitgestellte Abhängigkeitsreihenfolge, kann die restlichen Berechnungen jedoch in beliebiger Reihenfolge ausführen. Die Berechnungsreihenfolge könnte beispielsweise wie folgt lauten:

A = 1 + 1 
B = 2 + 2 
C = A + B

Diese Reihenfolge ist ebenfalls möglich:

B = 2 + 2 
A = 1 + 1 
C = A + B

Da A und B nicht voneinander abhängen, können sie gleichzeitig berechnet werden:

    B = 2 + 2 gleichzeitig mit A = 1 + 1
    C = A + B

Nebeneffekte

Das automatische Berechnen der Reihenfolge von Berechnungen durch eine Ausdrucksauswertung ist ein einfaches und leistungsfähiges Berechnungsmodell für Fälle, in denen keine expliziten Abhängigkeiten vom Ausdruck angegeben werden.

Dies hängt jedoch davon ab, ob Berechnungen neu angeordnet werden können. Da Ausdrücke Funktionen aufrufen und diese Funktionen den Zustand außerhalb des Ausdrucks beobachten können, indem externe Abfragen ausgegeben werden, ist es möglich, ein Szenario zu erstellen, in dem die Reihenfolge der Berechnung wichtig ist, aber nicht in der partiellen Reihenfolge des Ausdrucks aufgezeichnet wird. Beispielsweise kann eine Funktion den Inhalt einer Datei lesen. Wenn diese Funktion wiederholt aufgerufen wird, können externe Änderungen an dieser Datei beobachtet werden. Daher kann die Neuanordnung zu wahrnehmbaren Unterschieden im Programmverhalten führen. Abhängig von einer solchen beobachteten Auswertung bewirkt das Sortieren für die Richtigkeit eines M-Ausdrucks eine Abhängigkeit von bestimmten Implementierungsoptionen, die von der einen zur nächsten Auswertung variieren oder bei der gleichen Auswertung unter unterschiedlichen Umständen sogar voneinander abweichen können.

Unveränderlichkeit

Nachdem ein Wert berechnet wurde, ist er unveränderlich. Dies bedeutet, dass er nicht mehr geändert werden kann. Dadurch wird das Modell für die Auswertung eines Ausdrucks und die Ursache für das Ergebnis vereinfacht, da es nicht möglich ist, einen Wert zu ändern, nachdem er zum Auswerten eines nachfolgenden Teils des Ausdrucks verwendet wurde. Beispielsweise wird ein Datensatzfeld nur bei Bedarf berechnet. Nach der Berechnung bleibt es jedoch für die gesamte Lebensdauer des Datensatzes unveränderlich. Auch wenn beim Versuch, das Feld zu berechnen, ein Fehler ausgelöst wurde, wird derselbe Fehler bei jedem Versuch, auf dieses Datensatzfeld zuzugreifen, wieder ausgelöst.

Eine wichtige Ausnahme der Regel für die Unveränderlichkeit nach der Berechnung stellen Listen-, Tabellen- und Binärwerte dar, die über eine Streamingsemantik verfügen. Die Streamingsemantik ermöglicht es M, Datasets zu transformieren, die nicht alle gleichzeitig in den Arbeitsspeicher passen. Beim Streaming werden die beim Aufzählen einer bestimmten Tabelle, einer Liste oder eines Binärwerts zurückgegebenen Werte, bei jeder Anforderung bedarfsgesteuert erneut erzeugt. Da die Ausdrücke, die die Aufzählungswerte definieren, jedes Mal ausgewertet werden, wenn sie aufgezählt werden, kann die Ausgabe, die sie erzeugen, zwischen mehreren Enumerationen unterschiedlich sein. Dies bedeutet nicht, dass mehrere Enumerationen immer zu unterschiedlichen Werten führen, sondern nur, dass sie unterschiedlich sein können, wenn die verwendete Datenquelle oder M-Logik nicht deterministisch ist.

Beachten Sie außerdem, dass die Funktionsanwendung nicht identisch mit der Werterstellung ist. Bibliotheksfunktionen können den externen Zustand (z. B. die aktuelle Zeit oder die Ergebnisse einer Abfrage für eine Datenbank, die sich im Laufe der Zeit weiterentwickelt) verfügbar machen, indem diese nicht deterministisch zusammengeführt werden. Obwohl in M definierte Funktionen dieses nicht deterministische Verhalten nicht bieten, ist dies beim Aufrufen nicht deterministischer Funktionen dennoch möglich.

Eine letzte Quelle für nicht deterministisches Verhalten in M sind Fehler. Durch das Auftreten von Fehlern werden Auswertungen beendet (bis zu der Ebene, auf der sie von einem try-Ausdruck behandelt werden). Es ist in der Regel nicht erkennbar, ob a + b die Auswertung von a vor b oder die Auswertung von b vor a verursacht hat (Parallelität wird hier zur Vereinfachung ignoriert). Wenn jedoch der zuerst ausgewertete Teilausdruck einen Fehler auslöst, kann festgestellt werden, welcher der beiden Ausdrücke zuerst ausgewertet wurde.