Сравнение Tolk и FunC: подробно
Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.
Ниже очень большой список. У кого-нибудь хватит терпения дочитать его до конца?..
✅ Классические комментарии :)
FunC | Tolk |
---|---|
;; комментарий | // комментарий |
{- многострочный комментарий -} | /* многострочный комментарий */ |
✅ 2+2
— это 4, а не идентификатор. Идентификаторы могут быть только буквенно-цифровыми
В FunC почти любой символ может быть частью идентификатора.
Например, 2+2
(без пробела) — это идентификатор.
Вы даже можете объявить переменную с таким именем.
В Tolk пробелы не обязательны. 2+2
— это 4, как и ожидалось. 3+~x
это 3 + (~ x)
и так далее.
FunC | Tolk |
---|---|
return 2+2; ;; неопределенная функция `2+2` | return 2+2; // 4 |
Точнее, идентификатор может начинаться с [a-zA-Z$_]
и продолжиться с [a-zA-Z0-9$_]
. Обратите внимание, что ?
, :
и другие не являются допустимыми символами, found?
и op::increase
не являются допустимыми идентификаторами.
Вы можете использовать обратные кавычки, чтобы окружить идентификатор, и тогда он может содержать любые символы (аналогично Kotlin и некоторым другим языкам). Его потенциальное применение — разрешить использовать ключевые слова в качестве идентификаторов, на пример, в случае генерации кода по схеме.
FunC | Tolk |
---|---|
const op::increase = 0x1234; | const OP_INCREASE = 0x1234; |
;; even 2%&!2 is valid | // don't do like this :) |
✅ По умолчанию нечеткий, компилятор не будет отбрасывать вызовы пользовательских функций
FunC имеет спецификатор функции impure
. При его отсутствии функция рассматривается как чистая. Если ее результат не используется, ее вызов был удален компилятором.
Хотя такое поведение документировано, оно очень неожиданно для новичков. Например, различные функции, которые ничего не возвращают (например, выдают исключение при несовпадении), молча удаляются. Эту ситуацию портит тот факт, что FunC не проверяет и не валидирует тело функции, разрешая нечеткие операции внутри чистых функций.
В Tolk все функции по умолчанию являются нечистыми. Вы можете пометить функцию как чистую с помощью аннотации, и тогда в ее теле будут запрещены нечистые операции (исключения, изменение глобальных параметров, вызов нечистых функций и т. д.).
✅ Новы й синтаксис функций: ключевое слово fun
, атрибуты @
, типы справа (как в TypeScript, Kotlin, Python и т. д.)
FunC | Tolk |
---|---|
cell parse_data(slice cs) { } | fun parse_data(cs: slice): cell { } |
(cell, int) load_storage() { } | fun load_storage(): (cell, int) { } |
() main() { ... } | fun main() { ... } |
Типы переменных — также справа:
FunC | Tolk |
---|---|
slice cs = ...; | var cs: slice = ...; |
(cell c, int n) = parse_data(cs); | var (c: cell, n: int) = parse_data(cs); |
global int stake_at; | global stake_at: int; |
Модификаторы inline
и другие — с аннотациями:
FunC | Tolk |
---|---|
| @inline |
| @inline_ref |
global int stake_at; | global stake_at: int; |
forall
— таким образом:
FunC | Tolk |
---|---|
forall X -> tuple cons(X head, tuple tail) | fun cons<X>(head: X, tail: tuple): tuple |
реализация asm
— как в FunC, но при правильном выравнивании выглядит красивее:
@pure
fun third<X>(t: tuple): X
asm "THIRD";
@pure
fun iDictDeleteGet(dict: cell, keyLen: int, index: int): (cell, slice, int)
asm(index dict keyLen) "DICTIDELGET NULLSWAPIFNOT";
@pure
fun mulDivFloor(x: int, y: int, z: int): int
builtin;
Также есть атрибут @deprecated
, не вл ияющий на компиляцию, но для человека и IDE.
✅ get
вместо method_id
В FunC method_id
(без аргументов) фактически объявляет get-метод. В Tolk используется простой синтаксис:
FunC | Tolk |
---|---|
int seqno() method_id { ... } | get seqno(): int { ... } |
Допустимы как get methodName()
, так и get fun methodName()
.
Для method_id(xxx)
(на практике встречается редко, но допустимо) есть атрибут:
FunC | Tolk |
---|---|
| @method_id(1666) |
✅ Важно объявить типы параметров (хотя необязательно для локальных переменных)
// not allowed
fun do_smth(c, n)
// types are mandatory
fun do_smth(c: cell, n: int)
Если типы параметров обязательны, возвращаемый тип не является обязательным (это часто очевидно из-за многословности). Если он опущен, он выводится автоматически:
fun x() { ... } // auto infer from return statements
Для локальных переменных типы также необязательны:
var i = 10; // ok, int
var b = beginCell(); // ok, builder
var (i, b) = (10, beginCell()); // ok, two variables, int and builder
// types can be specified manually, of course:
var b: builder = beginCell();
var (i: int, b: builder) = (10, beginCell());
✅ Переменные не могут быть повторно объявлены в той же области
var a = 10;
...
var a = 20; // error, correct is just `a = 20`
if (1) {
var a = 30; // it's okay, it's another scope
}
Как следствие, частичное переназначение не допускается:
var a = 10;
...
var (a, b) = (20, 30); // error, releclaration of a
Обратите внимание, что это не проблема для loadUint()
и других методов. В FunC они возвращали измененный объект, поэтому шаблон var (cs, int value) = cs.load_int(32)
был довольно распространен. В Tolk такие методы изменяют объект: var value = cs.loadInt(32)
, поэтому повторное объявление вряд ли понадобится.
fun send(msg: cell) {
var msg = ...; // error, redeclaration of msg
// solution 1: intruduce a new variable
var msgWrapped = ...;
// solution 2: use `redef`, though not recommended
var msg redef = ...;
✅ Изменения в системе типов
Система типов FunC основана на Хиндли-Милнере. Это распространенный подход для функциональных языков, где типы выводятся из использования посредством унификации.
В Tolk v0.7 система типов переписана с нуля. Чтобы добавить логические значения, целые числа фиксированной ширины, допустимость значений NULL, структуры и обобщения, нам нужна статическая система типов (например, TypeScript или Rust). Поскольку Хиндли-Милнер будет конфликтовать с методами структур, бороться с надлежащими обобщениями и станет совершенно непрактичным для типов объединений (несмотря на заявления, что он был "разработан для типов объединений").
У нас есть следующие типы:
int
,bool
,cell
,slice
,builder
, нетипизированныйtuple
- типизированный кортеж
[T1, T2, ...]
- тензор
(T1, T2, ...)
- вызываемые
fun(TArgs) -> TResult
void
(каноничнее называтьunit
, ноvoid
надежнее)self
, для создания цепочечных методов, опи санных ниже; на самом деле это не тип, он может встречаться только вместо возвращаемого типа функции
Система типов подчиняется следующим правилам:
- типы переменных могут быть указаны вручную или выведены из объявлений и никогда не изменяются после объявления
- параметры функции должны быть строго типизированы
- возвращаемые типы функции, если не указаны, выводятся из операторов return, аналогично TypeScript; в случае рекурсии (прямой или косвенной) возвращаемый тип должен быть явно объявлен где-то
- поддерживаются универсальные функции
✅ Понятные и читаемые сообщения об ошибках при несоответствии типов
В FunC из-за Хиндли-Милнера ошибки несоответствия типов очень трудно понять:
error: previous function return type (int, int)
cannot be unified with implicit end-of-block return type (int, ()):
cannot unify type () with int
В языке Tolk они удобочитаемы для человека:
1) can not assign `(int, slice)` to variable of type `(int, int)`
2) can not call method for `builder` with object of type `int`
3) can not use `builder` as a boolean condition
4) missing `return`
...
✅ Тип bool
, приводящий boolVar к int
Внутри себя bool
по-прежнему -1 и 0 на уровне TVM, но с точки зрения системы типов bool
и int
теперь различны.
Операторы сравнения == / >= /...
возвращают bool
. Логические операторы && ||
возвращают bool
. Константы true
и false
имеют тип bool
.
Многие функции stdlib теперь возвращают bool
, а не int
(имея -1 и 0 во время выполнения):
var valid = isSignatureValid(...); // bool
var end = cs.isEndOfSlice(); // bool
Оператор !x
поддерживает как int
, так и bool
. Условие if
и подобные принимает как int
(!= 0), так и bool
.
Логические &&
и ||
принимают как bool
, так и int
, сохраняя совместимость с такими конструкциями, как a && b
, где a
и b
являются целыми числами (!= 0).
Арифметические операторы ограничены целыми числами, для bool разрешены только побитовые и логические:
valid && end; // ok
valid & end; // ok, bitwise & | ^ also work if both are bools
if (!end) // ok
if (~end) // error, use !end
valid + end; // error
8 & valid; // error, int & bool not allowed
Обратите внимание, что логические операторы && ||
(отсутствуют в FunC) всегда используют представление asm IF/ELSE.
В будущем для оптимизации их можно будет автоматически заменить на & |
, когда это безопасно (например: a > 0 && a < 10
).
Чтобы вручную оптимизировать потребление газа, вы по-прежнему можете использовать & |
(разрешено для bool), но помните, что они не являются сокращенными.
bool
можно привести к int
с помощью оператора as
:
var i = boolValue as int; // -1 / 0
Нет никаких преобразований во время выполнения. bool
гарантированно равен -1/0 на уровне TVM, поэтому это приведение только к типу.
Но, как правило, если вам нужно такое приведение, вероятно, вы делаете что-то неправильно (если только вы не делаете сложную побитовую оптимизацию).
✅ Универсальные функции и экземпляры, такие как f<int>(...)
В FunC были функции "forall":
forall X -> tuple tpush(tuple t, X value) asm "TPUSH";
Tolk представляет правильно сделанные универсальные функции. Их синтаксис напоминает основные языки:
fun tuplePush<T>(mutate self: tuple, value: T): void
asm "TPUSH";
Когда вызывается f<T>
, T
обнаруживается (в большинстве случаев) по предоставленным аргументам:
t.tuplePush(1); // detected T=int
t.tuplePush(cs); // detected T=slice
t.tuplePush(null); // error, need to specify "null of what type"
Также поддерживается синтаксис f<int>(...)
:
t.tuplePush<int>(1); // ok
t.tuplePush<int>(cs); // error, can not pass slice to int
t.tuplePush<int>(null); // ok, null is "null of type int"
Пользовательские функции также могут быть универсальными:
fun replaceLast<T>(mutate self: tuple, value: T) {
val size = self.tupleSize();
self.tupleSetAt(value, size - 1);
}
Вызов replaceLast<int>
и replaceList<slice>
приведет к ДВУМ сгенерированным функциям asm (fift).
На самом деле, они в основном напоминают "шаблонные" функции. При каждом уникальном вызове тело функции полностью клонируется под новым именем.
Может быть несколько общих параметров:
fun replaceNulls<T1, T2>(tensor: (T1, T2), v1IfNull: T1, v2IfNull: T2): (T1, T2) {
var (a, b) = tensor;
return (a == null ? v1IfNull : a, b == null ? v2IfNull : b);
}
Общий параметр T
может быть чем-то сложным.
fun duplicate<T>(value: T): (T, T) {
var copy: T = value;
return (value, copy);
}
duplicate(1); // duplicate<int>
duplicate([1, cs]); // duplicate<[int, slice]>
duplicate((1, 2)); // duplicate<(int, int)>
Или даже функции, это тоже работает:
fun callAnyFn<TObj, TResult>(f: fun(TObj) -> TResult, arg: TObj) {
return f(arg);
}
fun callAnyFn2<TObj, TCallback>(f: TCallback, arg: TObj) {
return f(arg);
}