Tolk 与 FunC 中 tilda 函数的可变性对比
- 无
~
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 和其他语言相同的调用:
FunC | Tolk |
---|---|
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; |
slice data = get_data() | val flag = getContractData() |
dict~udict_set(...); | dict.uDictSet(...); |
b~store_uint(x, 32); | b.storeInt(x, 32); |
b = b.store_int(x, 32); | b.storeInt(x, 32); |
b = b.store_int(x, 32) | b.storeInt(x, 32) |
为了实现这一点,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
}
将 mutate
和 self
结合起来,我们就得到了突变方法。
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)。没有带方法的类/结构。没有针对 slice
的 hash()
方法和针对 cell
的 hash()
方法。取而代之的是函数 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 可能会具备用真正的方法声明结构的能力,其通用性足以覆盖内置类型。但这还需要很长的路要走。