Изменчивость в Tolk по сравнению с функциями тильда в FunC
Эта страница переведена сообщест вом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.
- нет методов тильда
~
cs.loadInt(32)
изменяет срез и возвращает целое числоb.storeInt(x, 32)
изменяет конструкторb = b.storeInt()
также работает, поскольку он не только изменяет, но и возвращает- связанные методы работают аналогично JS, они возвращают
self
- все работает точно так же, как и ожидалось, похоже на JS
- нет накладных расходов на выполнение, точно такие же инструкции Fift
- пользовательские методы создаются легко
- тильда "~" вообще не существует в Tolk
Это радикальное изменение. Если в FunC есть .methods()
и ~methods()
, то в Tolk есть только точка, единственный способ вызвать .method()
. Метод может изменить объект, а может и нет. В отличие от "короткого" списка, это поведенческое и семантическое отличие от 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.
По умолчанию все аргументы копируются по значению (идентично 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
То же самое касается ячеек, срезов и всего остального:
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
к параметру, переданный аргумент будет изменен. Чтобы избежать неожиданных изменений, вы должны указать 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`
То же самое для срезов и любых других типов:
fun readFlags(mutate cs: slice) {
return cs.loadInt(32);
}
val flags = readFlags(mutate msgBody);
// msgBody.loadInt(32) will read the next integer
Это обобщение. Функция может иметь несколько параметров mutate:
fun incrementXY(mutate x: int, mutate y: int, byValue: int) {
x += byValue;
y += byValue;
}
incrementXY(mutate origX, mutate origY, 10); // both += 10
Вы можете спросить — это просто передача по ссылке? По сути, это так, но поскольку "ref" — это перегруженный термин в TON (у ячеек и срезов есть ссылки), было выбрано ключевое слово mutate
.
Параметр self
, превращающий функцию в метод
Когда первый параметр называется self
, он подчеркивает, что функция (все еще глобальная) является методом и должна вызываться через точку.
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 они обычно вызывались через тильду.
@pure
fun tuplePush<X>(mutate self: tuple, value: X): void
asm "TPUSH";
t.tuplePush(1);
return self
делает метод доступным для цепочки
Точно как return self
в Python или return this
в JavaScript. Это то, что делает методы типа 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 (как спецификатор inline
в FunC).
В будущем Tolk будет автоматически определять простые функции и выполнять встраивание true самостоятельно, на уровне AST. Такие функции даже не будут сгенерированы в Fift. Компилятор будет решать лучше, чем человек, встраивать ли, делать ссылку и т. д. Но Tolk понадобится некоторое время, чтобы стать таким умным :) На данный момент пожалуйста указывайте атрибуn @inline
.
Но self
— это не метод, это все еще функция! Я чувствую, что меня обманули
Абсолютно. Как и FunC, Tolk имеет только глобальные функции (начиная с v0.6). Нет классов/структур с методами. Нет методов hash()
для slice
и hash()
для cell
. Вместо этого есть функции 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, возможно, получит возможность объявлять структуры с помощью реальных методов, достаточно обобщенных, чтобы охватывать встроенные типы. Но для этого потребуется долгий путь.