5.4.6.4.1~23 表达式(上)

300 阅读8分钟

5.4.6.4 表达式

本节描述在任何 Reach 场景中均可用的表达式。Reach 程序中存在大量种类繁多的表达式。

本节的剩余部分将列举每种表达式

5.4.6.4.1 标识符引用

XYZ

标识符写作 ID ,是用于计算出绑定标识符的值的表达式

在本地步骤(即 onlyeach 表达式的主体部分)和共识步骤(即在 publishpay 语句中,在 commit 语句之前)中,标识符 this 具有特殊含义。具体细节参见 thisthis

5.4.6.4.2 函数应用

assert( amount <= heap1 )step( moveA )digest( coinFlip )interact.random()declassify( _coinFlip )

函数应用的格式为:EXPR_rator(EXPR_rand_0,...,EXPR_rand_n) ,其中: EXPR_rator 和 EXPR_rand_0 到 EXPR_rand_n 都是用于计算某个值的表达式; EXPR_rator 必须计算出 n 个相同类型的值,或是n个原语类型的参数; 扩展表达式(…expr)可能出现在函数应用的运算对象列表中,在这种情况下,expr 的元素会被拼接在恰当的位置。

new f(a) 等价于 f(a).new() ,在写面向类的程序时是一种非常方便的缩写。

5.4.6.4.3 类型

Reach 中的类型由以下标识符和构造函数表示:

  • Null 。
  • Bool ,表示一个布尔值。
  • UInt ,表示一个无符号整型数。UInt.max 是指可以分配给 UInt 的最大值。
  • Bytes(length) , 表示一个最大长度为 length 字节的字符串。
  • Digest ,表示一个摘要
  • Address ,表示一个账户地址
  • Token ,表示一个非网络代币
  • Fun([Domain_0, ..., Domain_N], Range) , 表示一个函数类型。
  • Tuple(Field_0, ..., FieldN) , 表示一个元组(参阅元组来构建元组 )。
  • Object({key_0: Type_0, ..., key_N: Type_N}) ,表示一个对象(参阅对象来构建对象)。
  • Struct([[key_0, Type_0], ..., [key_N, Type_N]]) ,表示一个结构体(参阅结构体来构建结构体)。
  • Array(Type_0,size) ,表示一个定长数组。 Type_0 必须是运行时可以存在的类型(即不是函数类型)(参阅数组来构建数组)。
  • Data({variant_0: Type_0, ..., variant_N: Type_N}) ,表示一个tagged union(或是汇总类型) (参阅数据来构建数据实例)。
  • Refine(Type_0, Predicate, ?Message) ,其中 Predicate 表示精化类型,是一个返回布尔值的一元函数,即 Type_0 的实例满足 Predicate 。当精化类型出现在被动位置(比如在一个 is 语句里面,或者是在一个作为参与者交互接口Fun 的域(Domain)中)时,它引入一个 assert;当它处于一个主动位置(即函数中的 Range )时,它引入了一个 assume 。Message 是一个可选字符串,在 Predicate 验证失败时显示。

比如,若 f 有如下类型: Fun([Refine(UInt, (x => x < 5))], Refine(UInt, (x => x > 10)))

那么 const z = f(y) 等价于: assert(y < 5);const z = f(y);assume(z > 10);

  • Refine(Type_0, PreCondition, PostCondition, ?Messages) ,其中 Type_0 是一个函数类型;PreCondition是一个一元函数,它接收域的元组并返回布尔值;PostCondition 是一个二进制函数,它接收域和范围的元组并返回一个布尔值;表示一个带有前置条件后置条件函数类型。前置条件是由 assert 确保,后置条件则使用 assume 确保。Message 是可选的双元组字节。前置条件验证失败时显示第一条消息,后置条件验证失败时显示第二条消息。

比如 Refine(Fun([UInt, UInt], UInt),([x, y] => x < y),(([x, y], z) => x + y < z)) 是一个函数,要求其第二个参数必须大于第一个参数,并且结果必须大于输入。

ObjectData 普遍用于 Reach 中生效的代数数据类型。 typeOf(x) // typeisType(t) // Boolis(x, t) // Bool

typeOf 原语函数与 typeof: 相同,它返回其参数的类型。

isType 函数的参数是一个类型,则其返回 true。任何满足 isType 的表达式都会被编译掉,并且在运行时不存在。

is 函数的第一个参数是第二个参数的类型,则其返回 true。这在 Refine 中被称为被动位置

5.4.6.4.4 字面值

100xdeadbeef007-1034.5432truefalsenull"reality bytes"'it just does'

字面值写作 VALUE ,是用于计算出给定表达式

空字面值可以写作 null

数字字面值可以是十进制、十六进制或八进制。如果在运行时用作 UInt 值,则数字字面值必须遵循 UInt 的位宽度;但如果它们仅在编译时出现,则可以是任意正数。Reach 为处理 Int 和带符号的 FixedPoint 数字提供了抽象。Int 可以通过对 UInt 类型的值使用一元 +- 运算符来定义。Reach 为定义带符号的 FixedPoint 数字提供语法糖,该语法糖以10为基数用十进制语法定义。

布尔字面值可以写成 truefalse

字符串字面值(又称字节字符串)可以写在双引号或单引号之间(不同样式之间没有区别),并使用与 JavaScript 相同的转义规则。

5.4.6.4.5 运算表达式

运算是特殊标识符,可能是一元运算,也可能是二元运算

! a  // 非- a  // 减+ a  // 加typeof avoid a

一元运算写作 UNAOP EXPR_rhs ,其中 EXPR_rhs 是一个表达式,UNAOP 是几种一元运算之一: ! - + typeof void 。除 typeof 之外的所有一元运算,均在标准库中有相应的命名版本。

对错误类型的使用一元运算是无效的。

应用于 UInt 类型的值时,一元 - 运算符和 + 运算符将其参数强制转换为 Int 类型。一元 - 运算符和 + 运算符是为 IntFixedPoint 类型的值定义的。

所有参数中的 void 等价于 null

a && ba || ba + ba - ba * ba / ba % ba | ba & ba ^ ba << ba >> ba == ba != ba === ba !== ba > ba >= ba <= ba < b

并非所有共识网络都支持位运算,并且位运算大大降低了验证效率。

二元表达式写作 EXPR_lhs BINOP EXPR_rhs ,其中 EXPR_lhs 和 EXPR_rhs 均为表达式,BINOP 是几种二元运算之一: && || + - * / % | & ^ << >> == != === !== > >= <= < 。运算符 ==(和 === )和 !=(和 !== )均可对所有原子值运算。数值运算符,如 +> ,只能对数字运算。由于 Reach 中所有数字均是整数,如 / 的运算符会截取其运算结果。 布尔运算符,如 && ,只能对布尔值进行运算。对错误类型的使用二元运算是无效的。

and(a, b)     // &&or(a, b)      // ||add(a, b)     // +sub(a, b)     // -mul(a, b)     // *div(a, b)     // /mod(a, b)     // %lt(a, b)      // <le(a, b)      // <=ge(a, b)      // >=gt(a, b)      // >lsh(a, b)     // <<rsh(a, b)     // >>band(a, b)    // &bior(a, b)    // |bxor(a, b)    // ^polyEq(a, b)  // ==, ===polyNeq(a, b) // !=, !==

所有二元表达式运算都在标准库中有相应的命名函数。尽管 &&|| 可能不会计算它们的第二个参数,但它们相应的命名函数 andor 总是会计算。

polyEq(a, b)    // 在所有类型上相等boolEq(a, b)    // 布尔值相等typeEq(a, b)    // 类型相等intEq(a, b)     // UInt 相等digestEq(a, b)  // 摘要相等addressEq(a, b) // 地址相等fxeq(a, b)      // FixedPoint 相等ieq(a, b)       // Int 相等

== 是一个在所有类型上运算的函数。两个参数必须类型相等。对于每个支持的类型,都有专门的函数用于相等性检查。

verifyArithmetictrue ,则算术运算会自动作出静态断言,即它们的参数不会溢出可用共识网络位宽度。如果为 false ,则连接器将动态地确保这一点。

5.4.6.4.6 异或

xor(false, false); // falsexor(false, true);  // truexor(true, false);  // truexor(true, true);   // false

xor(Bool, Bool) 只有在两个输入值不同时才会返回 true

5.4.6.4.7 带括号表达式

(a + b) - c

表达式可以用括号括起来,如 (EXPR) 。

5.4.6.4.8 元组

[ ][ 1, 2 + 3, 4 * 5 ]

元组字面写作 [ EXPR_0, ..., EXPR_n ] ,它是能计算出一个带有 n 个值的元组的表达式,其中 EXPR_0 到 EXPR_n 都是表达式

..expr 有可能出现在元组表达式内部,在这种情况下,展开的表达式必须计算为元组或数组,该结果也是按照对应表达式位置拼接而成的。

5.4.6.4.9 数组

const x = array(UInt, [1, 2, 3]);

将特定类型的单一类型值 tuple 转化为一个数组。

5.4.6.4.10 元素引用

arr[3]

引用写作 REF_EXPR[IDX_EXPR] ,其中 REF_EXPR 是用于计算出一个数组、一个元组或是一个结构体表达式。 IDX_EXPR 则是用于计算出一个小于数组长度的自然数的表达式。引用选择数组给定索引处的元素。下标从零开始。

若 REF_EXPR 是一个映射,且 IDX_EXPR 的计算结果是一个地址,那么这个引用的计算结果是类型为 Maybe(TYPE) 的值,其中 TYPE 是映射类型

5.4.6.4.11 数组和元组的长度:Tuple.length ,Array.length ,和 .length

Tuple.length(tup);tup.length;Array.length(arr);arr.length;

Tuple.length 返回给定元组的长度。

Array.length 返回给定数组的长度。

两者都可以缩写为 expr.length ,其中 expr 的计算结果为元组或数组。

5.4.6.4.12 数组和元组的更新:Tuple.set ,Array.set ,和 .set

Tuple.set(tup, idx, val);tup.set(idx, val);Array.set(arr, idx, val);arr.set(idx, val);

Tuple.set 返回一个与 tup 相同的新元组,只是索引 idx 替换为 val 。

Array.set 返回一个与 arr 相同的新数组,只是索引 idx 替换为 val 。

两者都可以缩写为 expr.set(idx, val) ,其中 expr 的计算结果为元组数组

5.4.6.4.13 可折叠操作

以下方法可用于任何可折叠容器,例如数组映射

Foldable.forEach && .forEach c.forEach(f)Foldable.forEach(c, f)Array.forEach(c, f)Map.forEach(c, f)

Foldable.forEach(c, f) 在容器 c 的元素上迭代函数 f ,并丢弃结果。这可以缩写为 c.forEach(f) 。

Foldable.all && .all Foldable.all(c, f)Array.all(c, f)Map.all(c, f)c.all(f) Foldable.all(c, f) 确定容器 c 里面的每个元素是否都符合要求 f 。

Foldable.any && .any Foldable.any(c, f)Array.any(c, f)Map.any(c, f)c.any(f)

Foldable.any(c, f) 确定容器 c 中是否至少有一个元素符合要求 f 。

Foldable.or && .or Foldable.or(c)Array.or(c)Map.or(c)c.or()

Foldable.or(c) 以布尔值返回容器的分离。

Foldable.and && .and Foldable.and(c)Array.and(c)Map.and(c)c.and() Foldable.and(c) 以布尔值返回容器的结合。

Foldable.includes && .includes Foldable.includes(c, x)Array.includes(c, x)Map.includes(c, x)c.includes(x)

Foldable.includes(c, x) 确定容器是否包含元素 x 。

Foldable.count && .count Foldable.count(c, f)Array.count(c, f)Map.count(c, f)c.count(f) Foldable.count(c, f)  返回容器 c 中满足要求 f 的元素个数。

Foldable.size && .size Foldable.size(c)Array.size(c)Map.size(c)c.size() Foldable.size(c) 返回 c 中元素的个数。

Foldable.min && .min Foldable.min(c)Array.min(c)Map.min(c)c.min() Foldable.min(arr) 返回 UInt 容器中的最小值。

Foldable.max && .max Foldable.max(c)Array.max(c)Map.max(c)c.max() Foldable.max(c) 返回 UInt 容器中的最大值。

Foldable.sum && .sum Foldable.sum(c)Array.sum(c)Map.sum(c)c.sum() Foldable.sum(c) 返回 UInt 容器中的总和。

Foldable.product && .product Foldable.product(c)Array.product(c)Map.product(c)c.product() Foldable.product(c) 返回 UInt 容器中的乘积。

Foldable.average && .average Foldable.average(c)Array.average(c)Map.average(c)c.average() Foldable.average(c) 返回 UInt 容器中的平均数。

5.4.6.4.14 数组的组运算

数组是可折叠容器。除了折叠的方法外,以下方法也可用于数组

Array.iota Array.iota(5) Array.iota(len) 返回一个长度为 len 的数组,其中每个元素都等于其索引值。例如,Array.iota(4) 返回 [0,1,2,3] 。给定的 len 在编译时必须计算为整数。

Array.replicate && .replicate Array.replicate(5, "five")Array_replicate(5, "five") Array.replicate(len,val) 返回一个长度为 len 的数组,其中每个元素都是 val 。例如:Array.replicate(4,"four") 返回 ["four","four","four","four"] 。给定的 len 在编译时必须计算为整数。

Array.concat && .concat Array.concat(x, y)x.concat(y) Array.concat(x, y) 将数组 x 和 y 拼接。这可以简写为 x.concat(y) 。

Array.empty Array_emptyArray.empty Array.empty 是一个没有元素的数组。它是 Array.concat 的标识元素,也可以写成 Array_empty 。

Array.zip&&.zip Array.zip(x, y)x.zip(y) Array.zip(x,y) 返回一个长度与 x 和 y(x 和 y 长度必须相同)相同的新数组,该新数组的元素是由 x 和 y 的元素构成的元组。这可以简写成 x.zip(y) 。

Array.map && .map Array.map(arr, f)arr.map(f) Array.map(arr,f) 返回一个长度与 arr 相等的新数组 arr_mapped ,其中对所有的 i 而言,arr_mapped[i] = f(arr[i]) 。例如:Array.iota(4).map(x => x + 1) 返回 [1, 2, 3, 4] 。这可以简写成 arr.map(f) 。

依此函数类推,可用于相同大小的任意数量的数组,这些数组要在 f 参数之前提供。例如: Array.iota(4).map(Array.iota(4), add) 返回 [0,2,4,6] 。

Array.reduce && .reduce Array.reduce(arr, z, f)arr.reduce(z, f) Array.reduce(arr,z,f) 返回函数 f 在给定数组上的左折叠,其初始值为 z 。例如: Array.iota(4).reduce(0,add) 返回 ((0+1)+2)+3=6 。 这可以简写成 arr.reduce(z,f) 。

依此函数类推,可用于相同大小的任意数量的数组,这些数组要在 z 参数之前提供。例如: Array.iota(4).reduce(Array.iota(4), 0, (x, y, z) => (z + x + y)) 返回 ((((0 + 0 + 0) + 1 + 1) + 2 + 2) + 3 + 3) 。

Array.indexOf && .indexOf Array.indexOf(arr, x)arr.indexOf(x) Array.indexOf(arr,x) 返回给定数组中第一个等于 x 的元素的索引。返回值的类型为 Maybe(UInt) 。若数组内没有符合条件的元素,则返回 None 。

5.4.6.4.15 映射的组运算

映射( Map )是一个 Foldable 容器。映射可以通过以下操作和 while 循环的常量内进行 Foldable 操作而被聚合。

Map.reduce&&.reduce Map.reduce(map, z, f)map.reduce(z, f) Map.reduce(map,z,f) 返回函数 f 在给定映射上的左折叠,初始值为 z 。例如:m.reduce(0, add) 汇总计算映射中的元素。 这可以简写为 map.reduce(z, f) 。

函数 f 必须满足如下特性:对所有的 z, a, b 而言,f(f(z, b), a) == f(f(z, a), b) ,因为运算的顺序是不可预测的。

5.4.6.4.16 对象

{ }{ x: 3, "yo-yo": 4 }{ [1 < 2 ? "one" : "two"]: 5 }

一个典型的对象格式为:{KEY_0: EXPR_0, ..., KEY_n: EXPR_n} ,其中 KEY_0 到 KEY_n 是标识符或者字符串字面值;EXPR_0 到 EXPR_n 是表达式。对象也是一个表达式,用于计算出带有字段 KEY_0 到 KEY_n 的对象

为方便起见,还存在其他对象字面语法,例如: { ...obj, z: 5 }

是一个对象拼接,将 obj 中的所有字段复制到对象中;其后也可以在这些字段之外添加额外字段。

{ x, z: 5 }

是 {x: x, z: 5} 的缩写,其中 x 是任意绑定字符串

5.4.6.4.17 结构体

const Posn = Struct([["x", UInt], ["y", UInt]]);const p1 = Posn.fromObject({x: 1, y: 2});const p2 = Posn.fromTuple([1, 2]);

结构体是元组对象的组合。它有命名的元素,与对象相似,但又像一个元组进行排序,所以它的元素可以通过名称或位置来访问。存在用于与非 Reach 远程对象连接的结构体,其中双方必须对值在运行时的表现形式达成一致。

可以通过调用结构体类型实例(如 Posn )的 fromTuple 方法,外加一个适当长度的元组,来构造结构体实例。

可以通过调用结构体类型实例(如 Posn )的 fromObject 方法,外加一个具有合适字段的对象,来构造结构体实例。

结构体可以通过 Struct 值(以及结构体类型实例,如上述例子中的 Posn )上的的 toTuple 和 toObject 方法转换为相应的元组或对象: assert(Posn.toTuple(p1)[0] == 1);assert(Struct.toObject(p2).y == 2);

5.4.6.4.16 字段引用

obj.x

字段引用写作 OBJ.FIELD ,其中 OBJ 是计算出一个对象结构体的表达式,FIELD 是一个有效标识符,用于访问 OBJ 对象的 FIELD 字段。

5.4.6.4.19 Object.set

Object.set(obj, fld, val);Object_set(obj, fld, val);{ ...obj, [fld]: val };

返回一个与 obj 相同的新对象, 只是字段 fld 被替换为 val 。

5.4.6.4.20 Object.setIfUnset

Object.setIfUnset(obj, fld, val);Object_setIfUnset(obj, fld, val);

返回一个与 obj相同的新对象,但如果 obj 中不存在 fld ,则添加值为 val 的 字段 fld 。

5.4.6.4.21 Object.has

Object.has(obj, fld);

返回一个布尔值,表示对象是否包含字段 fld 。这是静态已知的。

5.4.6.4.22 数据

const Taste = Data({Salty: Null,                    Spicy: Null,                    Sweet: Null,                    Umami: Null}); const burger = Taste.Umami();

const Shape = Data({ Circle: Object({r: UInt}),                     Square: Object({s: UInt}),                     Rect: Object({w: UInt, h: UInt}) }); const nice = Shape.Circle({r: 5});

数据实例写作 DATA.VARIANT(VALUE) ,其中 DATA 是 Data 类型,VARIANT 是 DATA 的变量中某一个的名称, VALUE 是匹配变量类型的值。作为一种特殊情况,当变量的类型为 Null 时,可以省略该值,如上文中 burger 的定义所示。

数据实例用于 switch 语句。

5.4.6.4.23 Maybe

const MayInt = Maybe(UInt); const bidA = MayInt.Some(42); const bidB = MayInt.None(null);

const getBid = (m) => fromMaybe(m, (() => 0), ((x) => x)); const bidSum = getBid(bidA) + getBid(bidB); assert(bidSum == 42);

选项类型可通过内置的 Data 类型 Maybe 在 Reach 中表示,它有两个变量:Some 和 None 。

Maybe 是由 export const Maybe = (A) => Data({None: Null, Some: A}); 定义的。

因此它是一个函数,返回一个专用于 Some 变量中特定类型的 Data 类型。

Maybe 实例可以通过 fromMaybe(mValue, onNone, onSome) 很方便地被使用,其中 onNone 是无参数的函数,当 mValue 为 None 时 onNone 会被调用;onSome 是 on 参数的函数,当 mValue 为 Some 时 onSome 的值会被调用,mValue 是 Maybe 的数据实例

const m = Maybe(UInt).Some(5); isNone(m); // false isSome(m); // true

isNone 是一种方便的方法,用于确定变量是否为 isNone 。

isSome 是一种方便的方法,用于确定变量是否为 isSome 。