价值观
单个数据称为值。从广义上讲,值有两大类:原始值,它们是原子的;结构化值,由原始值和其他结构化值构成。例如,值
复制
代码语言:javascript复制1
true
3.14159
"abc"
是原始的,因为它们不由其他值组成。另一方面,价值观
复制
代码语言:javascript复制{1, 2, 3}
[ A = {1}, B = {2}, C = {3} ]
使用原始值构造,在记录的情况下,使用其他结构化值构造。
表达式
一个表达式是用于构建值的公式。可以使用多种句法构造来形成表达式。下面是一些表达式的例子。每行都是一个单独的表达式。
复制
代码语言:javascript复制"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"
如上所示,最简单的表达式形式是表示值的文字。
更复杂的表达式是从其他表达式构建的,称为子表达式。例如:
复制
代码语言:javascript复制1 2
上面的表达式实际上是由三个表达式组成的。的1
和2
文字是母体表达的子表达式1 2
。
执行由在表达式中使用的语法结构中定义的算法被称为评估表达。每种表达式都有其计算方式的规则。例如,字面量表达式 like1
将产生一个常量值,而该表达式a b
将通过计算其他两个表达式(a
和b
)产生的结果值,并根据一些规则将它们加在一起。
环境和变量
表达式在给定的环境中进行评估。一个环境是一组命名值,称为变量。环境中的每个变量在环境中都有一个唯一的名称,称为标识符。
顶级(或根)表达式在全局环境中计算。全局环境由表达式求值器提供,而不是根据被求值的表达式的内容来确定。全局环境的内容包括标准库定义,并且可能会受到来自某些文档集的部分的导出的影响。(为简单起见,本节中的示例将假设一个空的全局环境。也就是说,假设没有标准库并且没有其他基于节的定义。)
用于评估子表达式的环境由父表达式决定。大多数父表达式类型将在它们被评估的相同环境中评估子表达式,但有些将使用不同的环境。全局环境是在其中计算全局表达式的父环境。
例如,record-initializer-expression使用修改后的环境评估每个字段的子表达式。修改后的环境包括记录的每个字段的变量,除了被初始化的字段。包括记录的其他字段允许字段依赖于字段的值。例如:
复制
代码语言:javascript复制[
x = 1, // environment: y, z
y = 2, // environment: x, z
z = x y // environment: x, y
]
类似地,let表达式为每个变量计算子表达式,环境包含 let 的每个变量,除了被初始化的变量。在松懈的表达评估与含有所有变量的环境之后的表达式:
复制
代码语言:javascript复制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实际上定义了两个环境,其中之一确实包含正在初始化的变量。这对于高级递归定义很有用,并在Identifier references 中进行了介绍。
为了形成子表达式的环境,新变量与父环境中的变量“合并”。以下示例显示了嵌套记录的环境:
复制
代码语言:javascript复制[
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 中的记录的环境:
复制
代码语言:javascript复制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
。
复制
代码语言:javascript复制[
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
]
标识符引用
一个标识符引用被用来指代一个可变的环境中。
标识符表达式: 标识符引用 标识符引用: 独占标识符引用 包含标识符引用
标识符引用的最简单形式是独占标识符引用:
唯一标识符引用: 标识符
它是用于错误异标识符引用来引用一个变量,它是不表达的环境的一部分,该标识符出现内,或者指的是当前正在初始化的标识符。
一种包容性的标识符引用可以用于访问,其包括被初始化的标识符的环境。如果它在没有标识符被初始化的上下文中使用,那么它相当于一个exclusive-identifier-reference。
包含标识符引用:
@
标识符
这在定义递归函数时很有用,因为函数的名称通常不在范围内。
复制
代码语言:javascript复制[
Factorial = (n) =>
if n <= 1 then
1
else
n * @Factorial(n - 1), // @ is scoping operator
x = Factorial(5)
]
与record-initializer-expression 一样,可以在let-expression 中使用inclusive-identifier-reference来访问包含正在初始化的标识符的环境。
评估顺序
考虑以下初始化记录的表达式:
复制
代码语言:javascript复制[
C = A B,
A = 1 1,
B = 2 2
]
计算时,此表达式产生以下记录值:
复制
代码语言:javascript复制[
C = 6,
A = 2,
B = 4
]
该表达式指出,为了执行A B
field的计算,必须知道C
fieldA
和 field的值B
。这是表达式提供的计算依赖关系排序的示例。M 评估器遵守表达式提供的依赖顺序,但可以按它选择的任何顺序自由执行剩余的计算。例如,计算顺序可以是:
复制
代码语言:javascript复制A = 1 1
B = 2 2
C = A B
或者它可能是:
复制
代码语言:javascript复制B = 2 2
A = 1 1
C = A B
或者,由于A
和B
不相互依赖,它们可以同时计算:
B = 2 2
同时与 A = 1 1
C = A B
副作用
允许表达式计算器在表达式没有明确依赖的情况下自动计算计算顺序是一个简单而强大的计算模型。
然而,它确实依赖于重新排序计算的能力。由于表达式可以调用函数,并且这些函数可以通过发出外部查询来观察表达式外部的状态,因此可以构建一个场景,其中计算顺序确实很重要,但不会在表达式的偏序中捕获。例如,函数可以读取文件的内容。如果重复调用该函数,则可以观察到对该文件的外部更改,因此,重新排序可能会导致程序行为出现明显差异。根据观察到的对 M 表达式正确性的评估顺序会导致对特定实现选择的依赖,这些选择可能因一个评估器而异,甚至可能在不同情况下对同一评估器有所不同。
不变性
一旦一个值被计算出来,它就是不可变的,这意味着它不能再被改变。这简化了评估表达式的模型,并且更容易对结果进行推理,因为一旦值被用于评估表达式的后续部分,就不可能更改该值。例如,仅在需要时才计算记录字段。然而,一旦计算,它在记录的生命周期内保持固定。即使尝试计算该字段引发错误,每次尝试访问该记录字段时也会再次引发相同的错误。
不可变一次计算规则的一个重要例外适用于列表和表格值。两者都有流语义。也就是说,重复枚举列表中的项目或表中的行会产生不同的结果。流语义支持构建 M 个表达式,这些表达式可以转换一次无法放入内存的数据集。
另外,还要注意功能应用是不一样的价值建设。库函数可能会暴露外部状态(例如当前时间或对随时间演变的数据库的查询结果),使它们变得不确定。虽然在 M 中定义的函数不会因此暴露任何此类非确定性行为,但如果它们被定义为调用其他非确定性函数,则它们可以。
M 中非确定性的最终来源是错误。错误在发生时停止计算(直到它们由 try 表达式处理的级别)。通常无法观察到是否a b
导致了a
beforeb
或b
before的评估a
(为简单起见,这里忽略并发)。但是,如果首先计算的子表达式引发错误,则可以确定首先计算两个表达式中的哪一个。