基本的な概念
このセクションでは、以降のセクションで紹介する基本的な概念について説明します。
値
単一のデータを "値" と呼びます。 大まかに言うと、値には 2 つの一般的なカテゴリ: "プリミティブ値" (アトミック) と "構造化値" (プリミティブ値と他の構造化値から構築される) があります。 たとえば、次の値
1
true
3.14159
"abc"
は、他の値で構成されていないため、プリミティブです。 一方、次の値
{1, 2, 3}
[ A = {1}, B = {2}, C = {3} ]
は、プリミティブ値を使用して構築され、レコードの場合は他の構造化値を使用して構築されます。
式
"式" は、値を構築するために使用される数式です。 式は、さまざまな構文構造を使用して形成できます。 次に、いくつかの式の例を示します。 各行は個別の式です。
"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"
上に示すように、最も単純な形式の式は、値を表すリテラルです。
より複雑な式は、"サブ式" と呼ばれる他の式から構築されます。 例:
1 + 2
上の式は、実際には 3 つの式で構成されています。 1
リテラルと 2
リテラルは、親式 1 + 2
のサブ式です。
式で使用される構文構造によって定義されたアルゴリズムの実行は、式の "評価" と呼ばれます。 各種類の式には、評価方法に関するルールがあります。 たとえば、1
のようなリテラル式では定数値が生成されますが、式 a + b
では、他の 2 つの式 (a
と b
) を評価して生成された値が取得され、何らかのルール セットに従って加算されます。
環境と変数
式は、特定の環境内で評価されます。 "環境" は、"変数" と呼ばれる一連の名前付きの値です。 環境内の各変数には、"識別子" と呼ばれる環境内で一意の名前が付けられています。
最上位レベル (ルート) 式は、"グローバル環境" 内で評価されます。 グローバル環境は、評価される式の内容から決定されるのではなく、式エバリュエーターによって指定されます。 グローバル環境の内容には標準ライブラリ定義が含まれており、一部のドキュメントのセクションからのエクスポートによって影響を受けることがあります (わかりやすくするために、このセクションの例では、空のグローバル環境を想定しています。つまり、標準ライブラリがないことと、他のセクションベースの定義がないことを前提としています)。
サブ式の評価に使用される環境は、親式によって決定されます。 ほとんどの種類の親式では、親式が評価されたのと同じ環境内でサブ式が評価されますが、別の環境が使用されるものもあります。 グローバル環境は、グローバル式が評価される "親環境" です。
たとえば、record-initializer-expression では、変更された環境で各フィールドのサブ式が評価されます。 変更された環境には、初期化中のものを除き、レコードの各フィールドの変数が含まれます。 レコードのその他のフィールドを含めることで、フィールドが値に依存できるようになります。 例:
[
x = 1, // environment: y, z
y = 2, // environment: x, z
z = x + y // environment: x, y
]
同様に、let-expression では、初期化中のものを除き、let の各変数を含む環境で各変数のサブ式が評価されます。 let-expression では、すべての変数を含む環境で、in に続く式が評価されます。
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
"レコード初期化子式" と "let 式" では両方とも、実際には "2 つの" 環境が定義され、その 1 つには初期化中の変数が含まれます。 これは高度な再帰定義に役立ち、「識別子参照」で説明されています。
サブ式の環境を形成するために、新しい変数は親環境の変数と "マージ" されます。 次の例は、入れ子になったレコードの環境を示しています。
[
a =
[
x = 1, // environment: b, y, z
y = 2, // environment: b, x, z
z = x + y // environment: b, x, y
],
b = 3 // environment: a
]
次の例は、let 内で入れ子になっているレコードの環境を示しています。
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
変数を環境にマージすると、変数間の競合が発生する可能性があります (環境内の各変数は一意の名前である必要があるため)。 競合は次のようにして解決されます。マージされる新しい変数の名前が親環境境内の既存の変数と同じである場合は、新しい環境で新しい変数が優先されます。 次の例では、内側 (より深い入れ子になった) 変数 x
が、外部変数 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
]
識別子参照
identifier-reference は、環境内の変数を参照するために使用されます。
identifier-expression:
identifier-reference
identifier-reference:
exclusive-identifier-reference
inclusive-identifier-reference
識別子参照の最も単純な形式は、exclusive-identifier-reference です。
exclusive-identifier-reference:
identifier
exclusive-identifier-reference では、識別子が含まれる式の環境の一部ではない変数を参照することはできません。
exclusive-identifier-reference では、参照先の識別子が record-initializer-expression または let-expression 内で定義されている場合、現在初期化されている識別子を参照することはできません。 代わりに、inclusive-identifier-reference を使用すると、初期化中の識別子を含む環境にアクセスできます。 その他の状況で inclusive-identifier-reference が使用される場合は、exclusive-identifier-reference と同等です。
inclusive-identifier-reference:
@
identifier
これは、関数の名前が通常はスコープ内にないため、再帰関数を定義するときに便利です。
[
Factorial = (n) =>
if n <= 1 then
1
else
n * @Factorial(n - 1), // @ is scoping operator
x = Factorial(5)
]
record-initializer-expression と同様に、inclusive-identifier-reference を let-expression 内で使用して、初期化中の識別子を含む環境にアクセスできます。
評価の順序
レコードを初期化する次の式について考えてみます。
[
C = A + B,
A = 1 + 1,
B = 2 + 2
]
この式を評価すると、次のレコード値が生成されます。
[
C = 6,
A = 2,
B = 4
]
この式では、フィールド C
に対して A + B
の計算を実行するために、フィールド A
とフィールド B
の両方の値がわかっている必要があることが示されます。 これは、式によって指定された計算の "依存関係の順序付け" の例です。 M エバリュエーターでは、式によって指定された依存関係の順序付けに従っていますが、選択した任意の順序で残りの計算を自由に実行できます。 たとえば、次のような計算順序が考えられます。
A = 1 + 1
B = 2 + 2
C = A + B
または、次のようになります。
B = 2 + 2
A = 1 + 1
C = A + B
また、A
と B
は相互に依存しないため、同時に計算できます。
B = 2 + 2
と A = 1 + 1
を同時実行
C = A + B
副作用
式によって明示的な依存関係が指定されていない場合に、式エバリュエーターで計算の順序を自動的に計算できるようにすることは、単純で強力な計算モデルです。
ただし、これは計算の順序を変更できることに依存しています。 式では関数を呼び出すことができ、これらの関数では、外部クエリを発行することによって式の外部の状態を観察できます。そのため、計算の順序が重要であるものの、式の一部の順序ではキャプチャされないというシナリオが生じる可能性があります。 たとえば、関数で、ファイルの内容を読み取ることができるとします。 その関数が繰り返し呼び出された場合、そのファイルに対する外部の変更を観察できるため、順序を変更すると、プログラムの動作に観察可能な差異が生じる可能性があります。 M 式の正確性について、このような観察される評価順序付けに依存すると、異なるエバリュエーター間で変化したり、さまざまな状況において同じエバリュエーターでも変化する可能性のある、特定の実装の選択に依存することになります。
不変性
値が計算されると、その値は "不変性" を持ちます。つまり、変更できなくなります。 したがって、式の後続部分の評価に使用された値を変更できないため、式を評価するためのモデルが単純化され、結果を推測しやすくなります。 たとえば、レコード フィールドは、必要な場合にのみ計算されます。 ただし、計算が完了すると、レコードの有効期間にわたって値が固定されたままになります。 フィールドを計算しようとしたときにエラーが発生した場合でも、そのレコード フィールドにアクセスしようとするたびに、同じエラーが再び発生します。
変更不可の計算済みルールの重要な例外は、ストリーミング セマンティクスを持つリスト、テーブル、バイナリ値に適用されます。 ストリーミング セマンティクスにより、M はメモリに収まらないデータ セットを一度に変換できます。 ストリーミングでは、特定のテーブル、リスト、またはバイナリ値を列挙するときに返される値は、要求されるたびにオンデマンドで生成されます。 列挙値を定義する式は列挙されるたびに評価されるため、生成される出力は複数の列挙値間で異なる場合があります。 これは、複数の列挙の結果が常に異なる値になるという意味ではなく、使用されているデータ ソースまたは M ロジックが非決定的である場合に値が異なる可能性があるということだけです。
また、関数アプリケーションは値の構築とは同じではありません。 ライブラリ関数では、外部の状態 (現在の時刻や、時間の経過と共に進化するデータベースに対するクエリの結果など) が公開され、これらが "非決定論的に" レンダリングされます。 したがって、M で定義された関数によって、このような非決定論的な動作は公開されませんが、非決定論的な他の関数を呼び出すように定義されている場合は、これらの動作が公開される可能性があります。
M での非決定論の最終ソースは、"エラー" です。 エラーが発生すると、評価は停止します (try 式によって処理されるレベルまで)。 通常、a + b
によって b
の前に a
が評価されたのか、または a
の前に b
が評価されたのかを知ることはできません (ここでは、簡略化のために同時実行については考えません)。 ただし、最初に評価されたサブ式でエラーが発生した場合は、2 つの式のどちらが最初に評価されたかを判断できます。