基本概念

本节讨论在后续节中显示的基本概念。

单个数据称为。 广义上讲,有两个常规类别的值:基元值和结构化值。前者是值的最基本形式,后者由基元值和其他结构化值构成。 例如,值

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"

如上所示,最简单的表达式形式是表示值的文本。

更复杂的表达式由其他表达式(称为 sub-expressions)组成。 例如:

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 使用包含所有变量的环境来计算后面的表达式:

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-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

或者,由于 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(为了简单,可以忽略并发性)。 但是,如果首先计算的子表达式引发错误,则可以确定两个表达式中哪个是首先计算的。