选择器

基本结构一节中我们已经看到,在定义合同模板时,可以通过#[liquid(signers)]属性标注成员,来表示该成员中包含合同的签署方。但是这种方式要求被标注的成员的数据类型要么为address类型,要么为包含address类型元素的迭代器。在实际应用中,这种限制会造成些许不便,例如,假设在投票场景中,使用下列Voter结构体类型来记录投票者信息:

1
2
3
4
5
6
 #[derive(InOut)]
 pub struct Voter {
     addr: address,
     voted: bool,
     choice: bool,
 }

同时,用于表示提案的Ballot合同模板的定义如下:

1
2
3
4
5
6
7
8
 #[liquid(contract)]
 pub struct Ballot {
     #[liquid(signers)]
     government: address,
     #[liquid(signers)]  // Oops...
     voters: Vec<Voter>,
     proposal: Proposal,
 }

可以看出,Ballot合同模板在定义中试图将government中的账户地址以及voters中的所有投票者的账户地址加入至签署方集合中,但是在处理voters时遇到了一点麻烦:voters的数据类型是元素类型为Voter的动态数组,并不满足#[liquid(signers)]属性对被标注成员的数据类型的要求,此时若直接编译将会报出类型不匹配错误。

出现上述问题的根本原因是,我们知道投票者的账户地址就在Voter类型的addr成员中,但是 Liquid 却无从知晓这一信息。为解决这一问题,Liquid 允许在使用#[liquid(signers)]属性时,向signers传递一个选择器参数。选择器是一个字符串,其中包含了特殊的语法,用于告知 Liquid 如何从成员中选择出我们想要的账户地址,例如对于上面的示例,可以修改为如下形式:

1
2
3
4
5
6
7
8
 #[liquid(contract)]
 pub struct Ballot {
     #[liquid(signers)]
     government: address,
     #[liquid(signers = "$[..].addr")]
     voters: Vec<Voter>,
     proposal: Proposal,
 }

选择器包含对象选择器函数选择器,我们接下来将分别对两者进行详细介绍。

对象选择器

对象选择器用于从数据成员中选择出特定的域,其语法构成如下:

语法 描述
根对象选择器
$
用于表示被#[liquid(signers)]属性标注的数据成员,是选择的起点。任何对象选择器都需要$为起始,除此之外,不能在任何其他位置使用$

举例:
$:与直接使用#[liquid(signers)]属性的作用相同。
子成员选择器
.<identifier>
在当前对象的数据类型为结构体或元组时,可以使用此选择器,用于选择当前对象的的下级子对象,其后需要跟随一个标识符。

举例:
$.0:选择根对象的第一个子对象,此时根对象为元组变量;
$.foo:选择根对象中名为“foo”的成员,此时根对象为结构体变量。
切片选择器
[<start>..<end>;<step>]
在当前对象可被迭代时,可以使用此选择器,用于选择当前对象中的部分或全部元素。语法中,start是整数起始下标,当start未提供时,表示从第一个元素开始选择;end是整数终止下标,当end未提供时,表示选择至最后一个元素;step是正整数步长,当step未提供时,默认步长为 1。选择时将会以step为步长选择下标位于[start, end)范围内的元素。当startend为负数时,用于表示用于表示“倒数第...个元素”。起始下标及终止下标均从0开始计算,当出现越界的情况时,Liquid 会直接抛出异常并回滚交易。

举例:
[1..5]:从第2个元素开始进行选择,直到遇到第6个元素;
[..]:选择全部元素;
[-3..-1]:从倒数第3个元素开始选择,直到遇到最后一个元素;
[..; 2]:选择所有下标为偶数的元素。
元素选择器
[<index>]
在当前对象可被迭代时,可以使用此选择器,用于选择当前对象中的某个元素。index为元素下标,需要为整数。当index为负数时,用于表示用于表示“倒数第...个元素”。此外,还可以同时提供多个下标,并使用,连接,即可以同时选择多个元素。下标从0开始计算,当出现越界的情况时,Liquid 会直接抛出异常并回滚交易。

举例:
[-1]:选择最后一个元素;
[0, 2, 3]:选择第1个、第3个及第4个元素
谓词选择器
(?<predict>)
在当前对象可被迭代时,可以使用此选择器,用于从当前对象对象中选择出符合特定条件的元素。predict是一个由条件表达式组成的谓词,其中可包含一个特殊的符号@,用于表示当前元素,其后可以继续嵌套对象选择器语法。谓词的返回值类型必须是布尔值。当前,谓词中支持使用相等(==)、不等(!=)、大于(>)、大于等于(>=)、小于(<)及小于等于(<=)关系运算符,同时还支持且(&&)、或(||)及非(!)逻辑运算符,并支持使用truefalse这两种布尔值常量及整数。各种运算符的优先级与 Rust 语言中对应的运算符优先级一致,并可以通过使用括号(())改变谓词中表达式的优先级。

举例:
(?@.count >= 3):从当前对象的各个元素中,选出子对象“count”的值大于等于3的元素;
(?false != (1 > 2 || (!false && @.0 < 2))):无实际意义,但是可以用来展示通过各种运算符的组合,谓词选择器可以用于表示复杂的逻辑。

通过将上述各类选择器级联组合,最终便能够从合同模板的数据成员中选择出所需要的账户地址。对于上节示例代码中的$[..].addr对象选择器。则可以理解为从voters中选择所有的元素,并从这些元素中各自选择出名为“addr”的子成员的值组成数组,最后将所有结果加入至签署方集合中。

注解

切片选择器、包含多个索引的元素选择器及谓词选择器的选择结果均是数组,之后若继续进行选择,则选择器将会作用于数组中的每个元素之上。例如在 $[..].addr" 对象选择器中,在使用切边选择器从 voters 中选择出所有的元素后,其后的 .addr 子成员选择器将会作用在每个元素上从而产生一个包含所有投票者地址的数组,而并非去选择数组自身的“addr”成员,请务必注意这种区别。

使用对象选择器时,要求选择结果的数据类型 T 需要满足下列要求之一:

  • Taddress类型;
  • T是一个集合类型(如VecHashMap等),但是&'a T类型必须实现了IntoIterator<Item = &'a address>特性,其中a为对应成员变量的生命周期。

函数选择器

对象选择器一般用于表达简单的选择逻辑,当选择逻辑比较复杂时,使用对象选择器可能会带来可读性方面的困扰。针对这种情况,Liquid提供了函数选择器,可以将复杂的选择逻辑实现在一个单独的函数中,并告知Liquid调用该函数进行选择。函数选择器的语法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#[liquid::collaboration]
mod voting {
    fn get_voted_voters(voters: &Vec<voter>) -> impl IntoIterator<Item = &address> {
        voters.iter().filter(|&voter| voter.voted).map(|&voter| voter.addr)
    }

    #[liquid(contract)]
    struct Ballot {
        #[liquid(signers = "::voting::get_voted_voters")]
        voters: Vec<Voter>,
        ...
    }
}

与对象选择器相同,函数选择器是一个字符串,但是其中包含了选择函数的绝对路径。绝对路径需要使用Rust语言中的语法表示,且必须以::开头。假设被#[liquid(signers)]属性标注的数据成员类型为T,则选择函数的签名需要是下列两种之一:

  • Fn(&T) -> impl IntoIterator<Item = &address>
  • Fn(&T) -> &address