基本概念
本節討論後續各節中所出現的基本概念。
值
單一項資料稱為一個「值」。 廣泛地說,有兩種一般類別的值:「基本值」(這是不可部分完成的),以及「結構化值」(由基本值和其他結構化值所構成)。 例如,值
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
上述運算式實際上是由三個運算式所組成。 1
和 2
常值都是父運算式 1 + 2
的子運算式。
執行運算式中所用語法結構定義的演算法,稱為「評估」運算式。 每種運算式都有其評估方式的規則。 例如,1
之類的常值運算式會產生常數值,而運算式 a + b
會採用產生的結果值,其產生方法是評估兩個其他運算式 (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
(事實上,record-initializer-expression 和 let-expression 實際上會定義「兩個」環境,其中一個包含正在初始化的變數。 這適用於進階遞迴定義,並在識別碼參考中介紹。
為了形成子運算式的環境,新變數會與父環境中的變數「合併」。 下列範例顯示巢狀記錄的環境:
[
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 若參考不屬於識別碼所在運算式的環境,則為錯誤的情況。
如果參考的識別碼定義在 record-initializer-expression 或 let-expression 內,exclusive-identifier-reference 若參考目前正在初始化的識別碼,則為錯誤的情況。 相反,inclusive-identifier-reference 可以用來存取環境,其中包含正在初始化的識別碼。 如果在任何其他情況下使用 inclusive-identifier-reference,則其相當於 exclusive-identifier-reference。
inclusive-identifier-reference:
@
識別碼
這在定義遞迴函式時很有用,因為函式的名稱通常不會在範圍內。
[
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 運算式正確性所觀察到的評估順序,會導致對特定實作選擇的相依性,而這些選項可能會隨不同評估工具而異,或甚至在相同評估工具上隨不同情況而異。
不變性
值計算出來之後就「不可變」,這表示它無法再變更。 如此可簡化評估運算式的模型,且可供更輕鬆地了解結果,因為當值用來評估運算式的後續部分之後,即無法變更該值。 例如,只有在需要時才會計算記錄欄位。 不過,一旦計算之後,它便會在記錄的存留期間都保持固定。 即使嘗試計算欄位引發了錯誤,也會在每次嘗試存取該記錄欄位時再次引發相同的錯誤。
immutable-once-calculated 規則的一個重要例外狀況適用於清單、資料表和二進位值,這些都具有「串流語意」。 串流語意可讓 M 轉換不能一次性全部納入記憶體的資料集。 使用串流時,列舉指定資料表、清單或二進位值時所傳回的值會在每次系統要求其時視需要產生。 由於定義列舉值的運算式會在每次列舉時進行評估,所以其所產生的輸出在多個列舉之間可能會有所不同。 這並不表示多個列舉一律會產生不同的值,只是如果正在使用的資料來源或 M 邏輯不確定,這些列舉可能會不同。
另請注意,函式應用程式與值結構「不同」。 程式庫函式可能會公開外部狀態 (例如目前時間,隨著時間而演變的資料庫查詢結果),使它們成為「不確定性」。 雖然在 M 中所定義函式不會像這樣公開任何不確定性的行為,但如果已定義,則其可以叫用其他不確定性的函式。
在 M 中,不確定性的最終來源是「錯誤」。 錯誤發生時會停止評估 (向上一直到 try 運算式處理的層級)。 其通常無法觀察 a + b
是否導致在 b
之前先評估 a
,或在 a
之前先評估 b
(為了簡單起見,在此忽略並行)。 不過,如果先評估的子運算式引發錯誤,則可以判斷先評估兩個運算式中的哪一個。