共用方式為


基本概念

本節討論後續各節中所出現的基本概念。

單一項資料稱為一個「值」。 廣泛地說,有兩種一般類別的值:「基本值」(這是不可部分完成的),以及「結構化值」(由基本值和其他結構化值所構成)。 例如,值

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

上述運算式實際上是由三個運算式所組成。 12 常值都是父運算式 1 + 2 的子運算式。

執行運算式中所用語法結構定義的演算法,稱為「評估」運算式。 每種運算式都有其評估方式的規則。 例如,1 之類的常值運算式會產生常數值,而運算式 a + b 會採用產生的結果值,其產生方法是評估兩個其他運算式 (ab) 並根據某一組規則將它們相加。

環境和變數

運算式會在指定的環境中進行評估。 「環境」是一組具名值,稱為「變數」。 環境中的每個變數在環境中都有唯一名稱,稱為「識別碼」

頂層 (或「根」) 運算式會在「全域環境」中進行評估。 全域環境是由運算式評估工具提供,而不是從所評估運算式的內容來決定。 全域環境的內容包含標準程式庫定義,且可能會受到來自某一組文件的區段匯出所影響。 (為了簡單起見,本節中範例會假設空的全域環境。也就是說,它會假設沒有標準程式庫,且沒有其他以區段為基礎的定義。)

用來評估子運算式的環境取決於父運算式。 大部分父運算式類型會在其評估所在的相同環境中評估子運算式,但有些會使用不同的環境。 全域環境是在其中評估全域運算式的「父環境」

例如,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-expressionlet-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-expressionlet-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-expressioninclusive-identifier-reference 可以用於 let-expression 內,以存取環境,其中包含正在初始化的識別碼。

評估順序

請考慮初始化記錄的下列運算式:

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

在評估時,此運算式會產生下列記錄值:

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

運算式指出,為了執行欄位 CA + B 計算,必須知道欄位 A 和欄位 B 的值。 這是運算式所提供的計算「相依性順序」範例。 M 評估工具遵守運算式所提供的相依性順序,但可隨意依其選擇的順序執行其餘計算。 例如,計算順序可以是:

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

或其可以是:

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

或者,因為 AB 彼此不相依,所以可以同時計算:

    B = 2 + 2 同時進行 A = 1 + 1
    C = A + B

副作用

針對沒有運算式所陳述明確相依性的情況,允許運算式評估工具自動計算計算的順序,這是一個簡單且功能強大的計算模型。

不過,它依賴能夠重新排序計算的順序。 因為運算式可以呼叫函式,而這些函式可能會藉由發出外部查詢而觀察到運算式以外的狀態,所以可以建構一個案例,讓計算順序有其重要性,但不會在運算式的部分順序中擷取。 例如,函式可能會讀取檔案的內容。 如果重複呼叫該函式,則可能會觀察到該檔案的外部變更,因此重新排序可能會導致程式行為有明顯的差異。 依賴這類針對 M 運算式正確性所觀察到的評估順序,會導致對特定實作選擇的相依性,而這些選項可能會隨不同評估工具而異,或甚至在相同評估工具上隨不同情況而異。

不變性

值計算出來之後就「不可變」,這表示它無法再變更。 如此可簡化評估運算式的模型,且可供更輕鬆地了解結果,因為當值用來評估運算式的後續部分之後,即無法變更該值。 例如,只有在需要時才會計算記錄欄位。 不過,一旦計算之後,它便會在記錄的存留期間都保持固定。 即使嘗試計算欄位引發了錯誤,也會在每次嘗試存取該記錄欄位時再次引發相同的錯誤。

immutable-once-calculated 規則的一個重要例外狀況適用於清單、資料表和二進位值,這些都具有「串流語意」。 串流語意可讓 M 轉換不能一次性全部納入記憶體的資料集。 使用串流時,列舉指定資料表、清單或二進位值時所傳回的值會在每次系統要求其時視需要產生。 由於定義列舉值的運算式會在每次列舉時進行評估,所以其所產生的輸出在多個列舉之間可能會有所不同。 這並不表示多個列舉一律會產生不同的值,只是如果正在使用的資料來源或 M 邏輯不確定,這些列舉可能會不同。

另請注意,函式應用程式與值結構「不同」。 程式庫函式可能會公開外部狀態 (例如目前時間,隨著時間而演變的資料庫查詢結果),使它們成為「不確定性」。 雖然在 M 中所定義函式不會像這樣公開任何不確定性的行為,但如果已定義,則其可以叫用其他不確定性的函式。

在 M 中,不確定性的最終來源是「錯誤」。 錯誤發生時會停止評估 (向上一直到 try 運算式處理的層級)。 其通常無法觀察 a + b 是否導致在 b 之前先評估 a,或在 a 之前先評估 b (為了簡單起見,在此忽略並行)。 不過,如果先評估的子運算式引發錯誤,則可以判斷先評估兩個運算式中的哪一個。