基本概念
本节讨论在后续节中显示的基本概念。
值
单个数据称为值。 广义上讲,有两个常规类别的值:基元值和结构化值。前者是值的最基本形式,后者由基元值和其他结构化值构成。 例如,值
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
上述表达式实际上由三个表达式组成。 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 使用包含所有变量的环境来计算后面的表达式:
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
(为了简单,可以忽略并发性)。 但是,如果首先计算的子表达式引发错误,则可以确定两个表达式中哪个是首先计算的。