基本概念和术语

Motoko是为使用actor的分布式编程而设计的。

在开始使用actor编写用于分布式处理的程序之前,您应该熟悉任何编程语言的一些基本构造块,尤其是Motoko。 为使您入门,本节介绍了以下关键概念和术语,这些概念和术语在本文档的其余部分中使用,并且对于学习在Motoko进行编程至关重要。

  • program 程序

  • declaration 声明

  • expression 表达式

  • value 值

  • variable 变量

  • type 类型

如果您有使用其他语言进行编程的经验或熟悉现代编程语言理论,那么您可能已经对这些术语及其用法感到满意。 在Motoko中如何使用这些术语没有什么独特之处。 但是,如果您不熟悉编程,本指南将通过使用简化的示例程序逐步介绍这些术语,这些示例程序避免使用actor或分布式编程。 在掌握了基本术语作为基础之后,您可以探索该语言的更多高级方面。 通过相应的更复杂的示例说明了更高级的功能。

本节介绍以下主题

Motoko program syntax / Motoko程序语法

Printing numbers and text, and using the base library / 显示数字和文本,并使用基本库

Declarations versus expressions / 声明与表达式

Lexical scoping of variables / 变量的词法作用域

Values and evaluation / 值和运算

Type annotations variables / 类型注释变量

Type soundness and type-safe evaluation / 类型安全性和类型安全性评估

Motoko程序语法

每个Motoko程序都是声明和表达式的免费组合,它们的句法类是不同的,但是却是相关的(有关精确的程序语法,请参见语言快速参考指南)。

对于我们部署在Internet Computer上的程序,有效程序由一个actor表达式组成,并以我们在'Actors和异步数据'中规定的特定语法表达(关键字actor)。

在例子中,我们将在本章中和下一章(Mutable state)中讨论一些程序,这些程序并不旨在作为InternetComputer的service(服务)。 而是,这些小型程序是组成Motoko编写的service(服务)的程序段,并且每个小型程序(通常)都可以作为Motoko程序独立运行,并可能带有一些可在终端显示的输出。

本节中的示例通过简单的表达式(例如算术)来说明基本原理。 有关Motoko的完整表达式语法的概述,请参阅Lanaguage快速参考。

首先,以下代码段由两个声明(变量x和y)组成,后跟一个表达式以形成一个程序:

let x = 1;
let y = x + 1;
x * y + x

在下面的讨论中,我们将使用此小型程序的这两个变量。

首先,该程序的类型为Nat(自然数),运行时,其计算结果为(自然数)值是3。

引入一个带有大括号(do { })和另一个变量(z)的块,我们可以对原始程序进行如下修改:

let z = do {
 let x = 1 
 let y = x + 1;
 x * y + x
};;

声明和表达式

回想一下,每个Motoko程序都是声明和表达式的自由组合,它们的句法类是不同的,但是相关。 在本节中,我们使用示例来说明它们的区别并适应它们的混合。

回顾上面介绍的示例程序:

let x = 1;
let y = x + 1;
x * y + x;

实际上,该程序是一个包含三个声明的清单:

  1. 不可变的变量x,通过声明让x = 1 ;,

  2. 不可变的变量y,通过声明让y = x + 1 ;,

  3. 还有一个未命名的隐式变量,用于保存最终表达式的值x * y + x。

这个表达式x * y + x说明了一个更通用的原理:每个表达式都可以在必要时视为一个声明,因为该语言会隐式声明一个带有该表达式结果值的未命名变量。

当表达式作为最终声明出现时,该表达式可以具有任何类型。 在这里,表达式x * y + x的类型为Nat(自然数)。

不是出现在末尾而是位于声明列表中的表达式必须具有单元类型()。

忽略声明列表中的非单位类型的表达式

我们总是可以通过显式地使用ignore来忽略任何未使用的结果值来克服这种单位类型的限制。 例如:

let x = 1;
ignore(x + 42);
let y = x + 1;
ignore(y * 42);
x * y + x;

声明和变量替换

声明可以是相互递归的,但是在声明不是相互递归的情况下,它们允许替换语义。

回想一下我们原来的例子:

let x = 1;
let y = x + 1;
x * y + x;

我们可以通过直接用变量的值代替变量来手动重写程序。

这样,我们产生以下表达式,它也是一个程序:

1 * (1 + 1) + 1

这也是与原始程序相同类型,具有相同行为(结果值为3)的有效程序。

我们还可以使用一个块来形成单个表达式。

从声明到块表达式

每个程序上面的许多程序都包含一个声明列表,就像上面的示例一样:

let x = 1;
let y = x + 1;
x * y + x

声明列表本身不是(立即)表达式,因此我们不能(立即)声明具有最终值(3)的另一个变量。

块表达式。 我们可以通过用匹配的花括号将其括起来,从而从这个声明列表中形成一个块表达式。 块仅允许作为控制流表达式的子表达式,如if,loop,case等。在所有其他位置,我们使用do {…}表示块表达式,以将块与对象文字区分开。 例如,do {}是类型为()的空块,而{}是记录类型为{}的空记录。

do {
 let x = 1;
 let y = x + 1;
 x * y + x
}

这也是程序,但是其中声明的变量x和y专用于我们引入的块的范围。

此块形式保留了声明列表的自主权及其对变量名的选择。

遵循词汇作用域的声明

在上面,我们看到了嵌套块保留了每个单独的声明列表的自主权及其对变量名的选择。 语言理论家称这个想法为词法作用域。 这意味着变量的作用域可以嵌套,但是在嵌套时它们可能不会干扰。

例如,下面的程序的结果为42,而不是2,因为最后一行的x和y,由第一行定义,而不是后面的块中的定义:

let x = 40; let y = 2;
ignore do {
 let x = 1;
 let y = x + 1;
 x * y + x
};
x + y

对于其他的没有词汇作用域的编程语言,上面这个程序可能有不同的结果。但是,现代语言普遍支持词汇作用域,即此处给出的含义。

除了数学上的清晰性之外,词法作用域的主要实际好处是安全性,以及它在构建成分安全系统中的使用。 特别是,Motoko具有非常强大的组合属性:例如,将程序嵌套在一个您不信任的程序中无法将其随机出现的内容重新赋予不同的含义。

值和运算

Motoko表达式接收到程序的(单个)控制线程后,就会马上执行,直到将其减小到(结果)值为止。

这样,它通常会在放弃环境控制堆栈的控制权之前将控制权传递给子表达式和子程序。

如果此表达式没有得到一个值的形式,则该表达式将无限求值。 稍后,我们会介绍递归函数和命令式控制流,它们各自都允许不终止。 目前,我们仅考虑终止那些会产生值的程序。

在上面的材料中,我们专注于产生自然数的表达式。 但是,作为更广泛的语言概述,我们简要总结如下的值的其他形式:

原始值

Motoko允许以下原始值形式:

  • 布尔值(真/假)

  • 整数(…​,-2, -1, 0, 1, 2, …​);有界和无界变量。

  • 自然数(0, 1, 2, …​); 有界和无界变量。

数字

默认情况下,整数和自然数是无界的,并且不会溢出。 取而代之的是,它们使用可以容纳任意有限数量的表示形式。

出于实际原因,Motoko还包括整数和自然数的有界类型,与默认版本不同。 每个有界变量都有固定的宽度(8、16、32、64之一),每个都有“溢出”的可能。 如果并且当此事件发生时,这是一个错误并导致程序停止。 除了在明确定义的情况下,Motoko中没有未经检查的,未经捕获的溢出,用于显式包装操作(由运算符中的%字符指示)。 该语言提供了原始的内置函数,可以在这些各种数字表示形式之间进行转换。

在'语言快速参考'中包含了原始类型的完整列表。

非原始值

在上述原始值和类型的基础上,该语言允许用户定义类型,以及以下每个非原始值形式和相关类型:

  • 元组,包括单位值(“空元组”)

  • 数组,具有可变的和不可变的变量

  • 对象,具有名称,无序字段和方法

  • 变体,具有命名的构造函数和可选的有效负载值

  • 函数值,包括可共享的函数

  • 异步值,也称为承诺或期货。

  • 错误值,携带了异常和系统故障的信息

我们将在后续章节中讨论这些形式的使用。 有关原始值和非原始值的精确语言定义,请参见语言快速参考。

单位类型与空类型

Motoko没有名为void的类型。 在许多情况下,读者可能会认为使用Java或C ++之类的返回类型是“无效”的,因此我们鼓励他们考虑使用书面()代替单位类型。

实际上,像空值一样,单位值通常表示为零。

与void类型不同,有一个单位值,但是像void返回值一样,该单位值在内部不携带任何值,因此,它始终携带零信息。

考虑单位值的另一种数学方法是将其作为无元素的元组-零元(“零元”)元组。 这些属性只有一个值,因此在数学上是唯一的,因此不需要在运行时表示。

自然数

这种类型的成员由常见的值组成---0、1、2,...----,但是,正如在数学中一样,他的成员不限于特定的最大值。 相反,这些值的运行时表示可容纳任意大小的数字,从而使其(几乎)不可能“溢出”。 (几乎是因为它与程序内存用尽是同一事件,在某些极端情况下某些程序总是会发生此事件)。

本子允许人们期望的通常的算术运算。 作为说明性示例,请考虑以下程序:

let x = 42 + (1 * 37) / 12: Nat

该程序的计算结果为值45,也为Nat类型。

类型安全性

对与每个进行了类型检查的Motoko表达式,我们都称为良好类型。 Motoko表达式的类型是从语言到开发人员对于未来行为的一种承诺。

首先,每个类型良好的程序都将在有定义行为的情况下进行评估。 就是说,“类型正确的程序不会出错”在这里适用。 对于那些不熟悉该短语更深层含义的人来说,这意味着存在有意义的(明确的)程序的精确空间,并且类型化的系统强制我们按照他的规则执行,并且所有类型良好的程序都具有精确的(明确的)程序意义。

此外,这些类型可以对程序的结果做出准确的预测。 如果产生控制权,程序将生成与原始程序一致的(结果)值。

在任何一种情况下,程序的静态和动态视图都由静态类型系统链接并与之一致。 该协议是静态类型系统的中心原理,由Motoko交付,是其设计的核心方面。

同一类型的系统还强制执行程序的静态视图和动态视图之间的异步交互,并且“在幕后”生成的结果消息在运行时绝不失配。 该协议在本质上类似于人们通常希望以一种键入语言进行的调用方/被调用方参数类型和返回类型协议。

类型注释和变量

变量将(静态)名称和(静态)类型与(动态)值相关联,这些值仅在运行时出现。

从这个意义上说,Motoko类型在程序源代码中提供了一种经过编译器验证的可信任文档。

比如下面这个简短的程序:

let x : Nat = 1

在这个例子中,编译器推断表达式中,值1的类型为Nat,而x的类型与它相同。

在这种情况下,我们可以省略此注释而不会更改程序的含义:

let x = 1

除了某些涉及运算符重载的未知情况外,类型的注释通常不会影响程序在运行时的含义。

如果省略它们,并且编译器接受该程序(如上述情况),则该程序具有与最初相同的含义(相同的行为)。

但是,有时编译器需要使用类型注释来推断其他假设,并从整体上检查程序。

当添加它们并且编译器仍接受该程序时,我们知道添加的注释与现有注释一致。

例如,我们可以添加其他(不是必需的)注释,然后编译器检查所​​有注释和其他推断的事实是否在总体上一致:

let x : Nat = 1 : Nat

但是,如果我们试图做一些与我们的注释类型不一致的事情,类型检查器将发出错误信号。

请看以下程序,这是个非良好类型的例子:

let x : Text = 1 + 1

stdin:1.16-1.21: type error [M0096], expression of type
  Nat
cannot produce expected type
  Text

类型注释Text与程序的其余部分不一致,因为1 + 1的类型是Nat而不是Text,并且这些类型与子类型无关。 因此,该程序为非良好类型的程序,并且编译器将发出错误信号(带有消息和位置),并且将不对其进行编译或执行。

类型错误和消息

从数学上讲,Motoko的类型系统是声明性的,这意味着它作为一种概念完全以形式逻辑形式存在,独立于任何执行程序。 同样,语言定义的其他关键方面(例如,其执行语义)也存在于执行程序之外。

但是,要设计此逻辑定义,进行试验并进行试错,我们希望与这种类型化的系统进行交互,并在此过程中犯下许多无害的错误。

类型检查器的错误消息会在误解或错误地应用类型系统的逻辑时尝试帮助开发人员,这在本书中进行了间接说明。

这些错误信息将随着时间不断进化,因此,在本文中我们将不包含特定的错误消息。 相反,我们将尝试在每个示例程序中解释一些可能的错误。

使用Motoko基础库

由于各种实际的语言工程原因,Motoko的设计努力使内置类型和操作最小化。

相反,只要有可能,Motoko基础库就会提供使Motoko看起来完整的类型和操作。 但是,此基础库仍在开发中,并且仍然不完整。

Motoko基础库列出了Motoko基础库中的一些模块,重点是示例中使用的核心功能,这些功能不太可能发生根本变化。 但是,所有这些基本库API肯定会随着时间(以不同程度)而变化,尤其是它们的大小和数量会不断增长。

要从基础库导入,请使用import关键字。 提供一个本地模块名称以进行介绍,在此示例中,“ D”代表“调试”,并提供一个URL,导入声明可以在该URL中找到导入的模块:

import D "mo:base/Debug";

D.print("hello world");

在这种情况下,我们导入带有mo:前缀的Motoko代码(不是其他模块形式)。 我们指定基本/路径,后跟模块的文件名Debug.mo减去其扩展名。

使用Debug.print和debug_show进行显示

上面,我们使用库Debug.mo中的函数print显示文本字符串:

print: Text -> ()

函数print接受文本字符串(文本类型)作为输入,并产生单位值(单元类型或())作为其输出。

由于单位值不包含任何信息,因此单位类型的所有值都相同,因此print函数实际上不会产生有意义的结果。 它有副作用,而不是结果。 Print函数具有将人类可读形式的文本字符串发送到输出终端的作用。 具有副作用(例如发出输出或修改状态)的函数通常称为不纯函数。 仅返回值而没有其他副作用的函数称为纯函数。 我们将在下面详细讨论返回值(单位值),并将其与void类型相关联,以使读者更加熟悉该概念。

最后,我们可以将大多数Motoko值转换为人类可读的文本字符串以进行调试,而无需手动编写这些转换。

debug_show 允许将大量的值转换为Text类型的值。

例如,我们可以将三元组(类型(文本,自然,文本))转换为调试文本,而无需自己编写自定义转换函数:

import D "mo:base/Debug";

D.print(debug_show(("hello", 42, "world")))

使用这些文本转换,我们可以在试验程序时显示大多数Motoko数据。

容纳不完整的代码

有时,在编写程序时,我们想运行一个不完整的版本,或者运行一个或多个执行路径丢失或完全无效的版本。

为了适应这些情况,我们使用基础Prelude库中的xxx,nyi和unreachable函数,如下所述。 每一个都包装了一个通用的陷阱机制,下面将进一步说明。

使用短期漏洞

对于仍在编写程序的开发人员而言,短期漏洞永远不会提交给源存储库,仅存在于单个开发会话中。

假定在之前已导入如下序言:

import P "mo:base/Prelude";

开发人员可以使用以下表达式填充任何缺少的表达式:

P.xxx()

结果将始终在编译时进行类型检查,并且始终在运行时(无论何时执行此表达式)让程序陷入陷阱。

记录长期漏洞

按照惯例,可以将长期漏洞视为“尚未实现”(nyi)功能,并使用Prelude模块中的类似功能进行标记:

P.nyi()

记录无法访问的代码路径

与上述情况相反,假设程序内部变量的内部逻辑是一致的,则有时将永远不会填充代码,因为永远都不会对代码进行评估。

要将代码路径记录为逻辑上不可能或无法访问,请使用基础库函数unreachable:

P.unreachable()

与上述情况一样,此函数在所有执行的程序中进行类型检查,并在评估时让所有执行的程序陷入陷阱。

执行陷阱,停止程序

上面的每种形式都是对assert原语的always-fail用法的简单包装:

assert false

在动态上,我们将此程序停止行为称为程序(生成的)陷阱,并且说该程序在执行此代码时陷入陷阱。 它将不再进一步执行下去。

最后更新于