跳到主要内容

Tolk 与 FunC 中 tilda 函数的可变性对比

TLDR
  • ~ tilda 方法
  • cs.loadInt(32) 修改片段并返回整数
  • b.storeInt(x,32) 修改了构建器
  • b = b.storeInt() 也能正常工作,因为它不仅修改,而且返回
  • 链式方法的工作原理与 JS 相同,它们返回 self
  • 一切如预期运行,与 JS 类似
  • 无运行时开销,完全相同的 Fift 指令
  • 轻松创建自定义方法
  • tilda ~ 在 Tolk 中根本不存在

这是一个巨大的变化。如果 FunC 有 .methods()~methods(),那么 Tolk 就只有 dot,一种也是唯一一种调用.methods()的方法。一个方法可以_mutate_ 一个对象,也可以不 mutate 一个对象。与列表中的 "简而言之 "不同,这是与 FunC 在行为和语义上的区别。

目标是实现与 JS 和其他语言相同的调用:

FunCTolk
int flags = cs~load_uint(32);var flags = cs.loadUint(32);
(cs, int flags) = cs.load_uint(32);var flags = cs.loadUint(32);
(slice cs2, int flags) = cs.load_uint(32);var cs2 = cs;
var flags = cs2.loadUint(32);
slice data = get_data()
             .begin_parse();
int flag = data~load_uint(32);
val flag = getContractData()
           .beginParse()
           .loadUint(32);
dict~udict_set(...);dict.uDictSet(...);
b~store_uint(x, 32);b.storeInt(x, 32);
b = b.store_int(x, 32);b.storeInt(x, 32);

// also works
b = b.storeUint(32);
b = b.store_int(x, 32)
     .store_int(y, 32);
b.storeInt(x, 32)
 .storeInt(y, 32);

// b = ...; also works

为了实现这一点,Tolk 提供了一种可变性概念,它是对 FunC 中 tilda 含义的概括。

默认情况下,所有参数都按值复制(与 FunC 相同)

fun someFn(x: int) {
x += 1;
}

var origX = 0;
someFn(origX); // origX remains 0
someFn(10); // ok, just int
origX.someFn(); // still allowed (but not recommended), origX remains 0

cells、 slices 等也是如此:

fun readFlags(cs: slice) {
return cs.loadInt(32);
}

var flags = readFlags(msgBody); // msgBody is not modified
// msgBody.loadInt(32) will read the same flags

这意味着,在调用函数时,可以确保原始数据不会被修改。

突变 关键字和突变函数

但如果在参数中添加 mutate 关键字,传递的参数就会发生变化。为了避免意外突变,在调用它时也必须指定 mutate

fun increment(mutate x: int) {
x += 1;
}

// it's correct, simple and straightforward
var origX = 0;
increment(mutate origX); // origX becomes 1

// these are compiler errors
increment(origX); // error, unexpected mutation
increment(10); // error, not lvalue
origX.increment(); // error, not a method, unexpected mutation
val constX = getSome();
increment(mutate constX); // error, it's immutable, since `val`

slices 和其他类型也是如此:

fun readFlags(mutate cs: slice) {
return cs.loadInt(32);
}

val flags = readFlags(mutate msgBody);
// msgBody.loadInt(32) will read the next integer

这是一种概括。一个函数可能有多个突变参数:

fun incrementXY(mutate x: int, mutate y: int, byValue: int) {
x += byValue;
y += byValue;
}

incrementXY(mutate origX, mutate origY, 10); // both += 10

你可以询问 — — 它是否只是通过引用? 它实际上是有效的,但由于“ref”在TON中是一个过载的术语(ells and slices have refs),选择了一个关键词mutate

将函数转化为方法的 self 参数

当第一个参数被命名为 self 时,它强调函数(仍然是全局函数)是一个方法,应通过 dot 来调用。

fun assertNotEq(self: int, throwIfEq: int) {
if (self == throwIfEq) {
throw 100;
}
}

someN.assertNotEq(10);
10.assertNotEq(10); // also ok, since self is not mutating
assertNotEq(someN, 10); // still allowed (but not recommended)

self 在没有 mutate 的情况下是 不可变的(与所有其他参数不同)。可以把它想象成 "只读方法"。

fun readFlags(self: slice) {
return self.loadInt(32); // error, modifying immutable variable
}

fun preloadInt32(self: slice) {
return self.preloadInt(32); // ok, it's a read-only method
}

mutateself 结合起来,我们就得到了突变方法。

mutate self 是一个通过点调用的方法,用于改变一个对象

具体如下

fun readFlags(mutate self: slice) {
return self.loadInt(32);
}

val flags = msgBody.readFlags(); // pretty obvious

fun increment(mutate self: int) {
self += 1;
}

var origX = 10;
origX.increment(); // 11
10.increment(); // error, not lvalue

// even this is possible
fun incrementWithY(mutate self: int, mutate y: int, byValue: int) {
self += byValue;
y += byValue;
}

origX.incrementWithY(mutate origY, 10); // both += 10

如果查看一下 stdlib,你会发现很多函数实际上都是 mutate self,也就是说,它们是修改对象的方法。元组、字典等等。在 FunC 中,它们通常通过 tilda 调用。

@pure
fun tuplePush<X>(mutate self: tuple, value: X): void
asm "TPUSH";

t.tuplePush(1);

return self 使方法可链式运行

就像 Python 中的 return self 或 JavaScript 中的 return this 一样。这也是 storeInt() 等方法可以链式处理的原因。

fun storeInt32(mutate self: builder, x: int): self {
self.storeInt(x, 32);
return self;

// this would also work as expected (the same Fift code)
// return self.storeInt(x, 32);
}

var b = beginCell().storeInt(1, 32).storeInt32(2).storeInt(3, 32);
b.storeInt32(4); // works without assignment, since mutates b
b = b.storeInt32(5); // and works with assignment, since also returns

请注意返回类型,它是 self。目前,您应该指定它。如果不指定,编译就会失败。也许将来会正确。

mutate self 和 asm 函数

对于用户定义的函数来说,这一点显而易见,但人们可能会感兴趣,如何制作一个具有这种行为的 asm 函数?要回答这个问题,我们应该先了解一下编译器内部的突变工作原理。

当一个函数有 mutate 参数时,它实际上是隐式返回参数,并将参数隐式赋值给参数。举例说明效果更好:

// actually returns (int, void)
fun increment(mutate x: int): void { ... }

// actually does: (x', _) = increment(x); x = x'
increment(mutate x);

// actually returns (int, int, (slice, cell))
fun f2(mutate x: int, mutate y: int): (slice, cell) { ... }

// actually does: (x', y', r) = f2(x, y); x = x'; y = y'; someF(r)
someF(f2(mutate x, mutate y));

// when `self`, it's exactly the same
// actually does: (cs', r) = loadInt(cs, 32); cs = cs'; flags = r
flags = cs.loadInt(32);

因此,asm 函数应将 self 放在返回值之前的堆栈中:

// "TPUSH" pops (tuple) and pushes (tuple')
// so, self' = tuple', and return an empty tensor
// `void` is a synonym for an empty tensor
fun tuplePush<X>(mutate self: tuple, value: X): void
asm "TPUSH";

// "LDU" pops (slice) and pushes (int, slice')
// with asm(-> 1 0), we make it (slice', int)
// so, self' = slice', and return int
fun loadMessageFlags(mutate self: slice): int
asm(-> 1 0) "4 LDU";

请注意,要返回 self,无需做任何特殊处理,只需指定返回类型即可。剩下的就交给编译器吧。

// "STU" pops (int, builder) and pushes (builder')
// with asm(op self), we put arguments to correct order
// so, self' = builder', and return an empty tensor
// but to make it chainable, `self` instead of `void`
fun storeMessageOp(mutate self: builder, op: int): self
asm(op self) "32 STU";

您不太可能需要使用这些技巧。最有可能的情况是,您只需为现有函数编写封装程序:

// just do like this, without asm, it's the same effective

@inline
fun myLoadMessageFlags(mutate self: slice): int {
return self.loadUint(4);
}

@inline
fun myStoreMessageOp(mutate self: builder, flags: int): self {
return self.storeUint(32, flags);
}

简单函数/方法是否需要 @inline

现在,最好这样做,没错。在上述大多数示例中,为了清晰起见,省略了 @inline。目前,如果没有 @inline,它将是一个单独的 TVM 续程,有跳入/跳出。如果使用 @inline,就会生成一个函数,但会被 Fift 内联(就像 FunC 中的 inline specifer)。

未来,Tolk 将自动检测简单函数,并在 AST 层面上执行真正的内联。这样的函数甚至都不需要编入 Fift。编译器将比人类更好地决定是否内联、是否进行 ref 等。但 Tolk 要变得如此聪明还需要一些时间:)现在,请指定 @inline 属性。

self 不是方法,它仍然是函数!我觉得我被骗了

当然可以。与 FunC 一样,Tolk 也只有全局函数(截至 v0.6)。没有带方法的类/结构。没有针对 slicehash() 方法和针对 cellhash()方法。取而代之的是函数 sliceHash()cellHash(),它们可以像函数一样调用,也可以用点调用(首选):

fun f(s: slice, c: cell) {
// not like this
s.hash();
c.hash();
// but like this
s.sliceHash();
c.cellHash();
// since it's the same as
sliceHash(s);
cellHash(s);
}

未来,在对类型系统进行了大量工作,并对 FunC 内核进行了全面重构之后,Tolk 可能会具备用真正的方法声明结构的能力,其通用性足以覆盖内置类型。但这还需要很长的路要走。