Функции
Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.
Программа FunC по сути является списком объявлений/определений функций и объявлений глобальных переменных. В этом разделе рассматривается первая тема.
Любое объявление или определение функции начинается с общего шаблона, а затем следует одно из трех:
-
одиночный
;
, что означает, что функция объявлена, но еще не определена. Она может быть определена позж е в том же файле или в каком-либо другом файле, который передается компилятору FunC перед текущим. Например,int add(int x, int y);
это простое объявление функции с именем
add
типа(int, int) -> int
. -
определение тела функции ассемблера. Это способ определения функций с помощью примитивов TVM низкого уровня для последующего использования в программе FunC. Например,
int add(int x, int y) asm "ADD";
это ассемблерное определение той же функции
add
типа(int, int) -> int
, которое будет транслироваться в код операции TVMADD
. -
обычное определение тела функции блочного оператора. Это обычный способ определения функций. Например,
int add(int x, int y) {
return x + y;
}это обычное определение функции
add
.
Объявление функции
Как уже было сказано, любое объявление или определение функции начинается с общего шаблона. Ниже приведен:
[<forall declarator>] <return_type> <function_name>(<comma_separated_function_args>) <specifiers>
где [ ... ]
соответствует необязательной записи.
Имя функции
Имя функции может быть любым идентификатором, а также может начинаться с символов .
или ~
. Значение этих символов объясняется в разделе операторов.
Например, udict_add_builder?
, dict_set
и ~dict_set
являются допустимыми и разными именами функций. (Они определены в stdlib.fc.)
Специальные имена функций
FunC (фактически ассемблер Fift) имеет несколько зарезервированных имен функций с предопределенными идентификаторами.
main
иrecv_internal
имеют id = 0recv_external
имеет id = -1run_ticktock
имеет id = -2
Каждая программа должна иметь функцию с id 0, то есть функцию main
или recv_internal
.
run_ticktock
вызывается в транзакциях ticktock специальных смарт-контрактов.
Внутренние получения
recv_internal
вызывается, когда смарт-контракт получает входящее внутреннее сообщение.
При запуске TVM в стеке есть несколько переменных, задавая аргументы в recv_internal
, мы даем коду смарт-контракта информацию о некоторых из них. Те аргументы, о которых код не будет знать, просто будут лежать внизу стека и никогда не будут затронуты.
Итак, каждое из следующих объявлений recv_internal
является правильным, но те, у которых меньше переменных, будут тратить немного меньше газа (каждый неиспользованный аргумент добавляет дополнительные инструкц ии DROP
)
() recv_internal(int balance, int msg_value, cell in_msg_cell, slice in_msg) {}
() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) {}
() recv_internal(cell in_msg_cell, slice in_msg) {}
() recv_internal(slice in_msg) {}
Внешние получения
recv_external
предназначено для входящих внешних сообщений.
Тип возврата
Тип возврата может быть любым атомарным или составным типом, как описано в разделе типы. Например,
int foo();
(int, int) foo'();
[int, int] foo''();
(int -> int) foo'''();
() foo''''();
являются допустимыми объявлениями функций.
Вывод типа также допускается. Например,
_ pyth(int m, int n) {
return (m * m - n * n, 2 * m * n, m * m + n * n);
}
является допустимым определением функции pyth
типа (int, int) -> (int, int, int)
, которая вычисляет пифагоровы тройки.
Аргументы функции
Аргументы функции разделяются запятыми. Допустимые объявления аргумента следующие:
- Обычное объявление: тип + имя. Например,
int x
— это объявление аргумента типаint
и имениx
в объявлении функции() foo(int x);
- Объявление неиспользуемого аргумента: только тип. Например,
это допустимое определение функции типа
int first(int x, int) {
return x;
}(int, int) -> int
- Аргумент с выведенным объявлением типа: только имя.
Например,
это допустимое определение функции типа
int inc(x) {
return x + 1;
}int -> int
. Типint
дляx
выводится средством проверки типов.
Обратите внимание, что хотя функция может выглядеть как функция нескольких аргументов, на самом деле это функция одного аргумента тензорного типа. Чтобы увидеть разницу, обратитесь к применению функции. Тем не менее, компоненты тензора аргумента традиционно называются аргументами функции.
Вызовы функций
Немодифицирующие методы
Немодифицирующая функция поддерживает короткую форму вызова функции с .
example(a);
a.example();
Если у функции есть хотя бы один аргумент, ее можно вызвать как немодифицирующий метод. Например, store_uint
имеет тип (builder, int, int) -> builder
(второй аргумент — это значение для сохранения, а третий — длина в битах). begin_cell
— это функция, которая создает новый builder. Следующие коды эквивалентны:
builder b = begin_cell();
b = store_uint(b, 239, 8);
builder b = begin_cell();
b = b.store_uint(239, 8);
Таким образом, первый аргумент функции может быть передан ей, будучи расположенным перед именем функции, если он разделен .
. Код можно упростить еще больше:
builder b = begin_cell().store_uint(239, 8);
Также возможны множественные вызовы методов:
builder b = begin_cell().store_uint(239, 8)
.store_int(-1, 16)
.store_uint(0xff, 10);
Модифицирующие функции
Модифицирующая функция поддерживает краткую форму с операторами ~
и .
.
Если первый аргумент функции имеет тип A
, а возвращаемое значение функции имеет вид (A, B)
, где B
— некоторый произвольный тип, то функцию можно вызвать как модифицирующий метод.
Модифицирующие вызовы функций могут принимать некоторые аргументы и возвращать некоторые значения, но они изменяют свой первый аргумент, то есть присваивают первый компонент возвращаемого значения переменной из первого аргумента.
a~example();
a = example(a);
Например, предположим, что cs
— это срез ячейки, а load_uint
имеет тип (slice, int) -> (slice, int)
: он принимает срез ячейки и количество бит для загрузки и возвращает остаток среза и загруженное значение. Следующие коды эквивалентны:
(cs, int x) = load_uint(cs, 8);
(cs, int x) = cs.load_uint(8);
int x = cs~load_uint(8);
В некоторых случаях мы хотим использовать функцию в качестве модифицирующего метода, который не возвращает никакого значения и изменяет только первый аргумент. Это можно сделать, используя типы единиц измерения, следующим образом: предположим, мы хотим определить функцию inc
типа int -> int
, которая увеличивает целое число, и использовать ее в качестве метода модификации. Затем мы должны определить inc
как функцию типа int -> (int, ())
:
(int, ()) inc(int x) {
return (x + 1, ());
}
При таком определении его можно использовать как модифицирующий метод. Следующий пример увеличит x
.
x~inc();
.
и ~
в именах функций
Предположим, мы хотим использовать inc
как немодифицирующий метод. Мы можем написать что-то вроде этого:
(int y, _) = inc(x);
Однако можно переопределить определение inc
, используя модифицирующий метод.
int inc(int x) {
return x + 1;
}
(int, ()) ~inc(int x) {
return (x + 1, ());
}
А затем назовем это так:
x~inc();
int y = inc(x);
int z = x.inc();
Первый вызов изменит x; второй и третий — нет.
Подводя итог, когда функция с именем foo
вызывается как немодифицирующий или модифицирующий метод (т. е. с синтаксисом .foo
или ~foo
), компилятор FunC использует определение .foo
или ~foo
соответственно, если такое определение представлено, а если нет, то использует определение foo
.
Спецификаторы
Существует три типа спецификаторов: impure
, inline
/inline_ref
и method_id
. Один, несколько или ни одного из них можно поместить в объявление функции, но в настоящее время они должны быть представлены в правильном порядке. Например, не допускается указывать impure
после inline
.
Спецификатор с побочными эффектами
Спецификатор impure
означает, что функция может иметь некоторые побочные эффекты, которые нельзя игнорировать. Например, мы должны указать спецификатор impure
, если функция может изменять хранилище контракта, отправлять сообщения или выдавать исключение, когда некоторые данные недействительны, и функция предназначена для проверки этих данных.
Если impure
не указан и результат вызова функции не используется, то компилятор FunC может и удалить этот вызов функции.
Например, в функции stdlib.fc
int random() impure asm "RANDU256";
определено. impure
используется, потому что RANDU256
изменяет внутреннее состояние генератора случайных чисел.
Встроенный спецификатор
Если функция имеет спецификатор inline
, ее код фактически подставляется в каждом месте, где вызывается функция. Само собой разумеется, что рекурсивные вызовы встроенных функций невозможны.
Например,
(int) add(int a, int b) inline {
return a + b;
}
поскольку функция add
отмечена спецификатором inline
. Компилятор попытается заменить вызовы add
фактическим кодом a + b
, избегая накладных расходов на вызов функции.
Вот еще один пример того, как можно использовать встроенный спецификатор, взятый из ICO-Minter.fc:
() save_data(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) impure inline {
set_data(begin_cell()
.store_coins(total_supply)
.store_slice(admin_address)
.store_ref(content)
.store_ref(jetton_wallet_code)
.end_cell()
);
}
Спецификатор Inline_ref
Код функции со спецификатором inline_ref
помещается в отдельную ячейку, и каждый раз, когда вызывается функция, TVM выполняет команду CALLREF
. Таким образом, это похоже на inline
, но поскольку ячейку можно повторно использовать в нескольких местах, не дублируя ее, почти всегда более эффективно с точки зрения размера кода использовать спецификатор inline_ref
вместо inline
, если только функция не вызывается ровно один раз. Рекурсивные вызовы встроенных функций по-прежнему невозможны, поскольку в ячейках TVM нет циклических ссылок.
method_id
Каждая функция в программе TVM имеет внутренний целочисленный идентификатор, по которому она может быть вызвана. Обычные функции нумеруются последующими целыми числами, начиная с 1, но get методы контракта нумеруются хэшами crc16 их имен. спецификатор method_id(<some_number>)
позволяет присвоить идентификатору функции указанное значение, а method_id
использует значение по умолчанию (crc16(<function_name>) & 0xffff) | 0x10000
. Если функция имеет спецификатор method_id
, то она может быть вызвана в lite-client или ton-explorer как get-метод по своему имени.
Например:
int get_counter() method_id {
load_data();
return ctx_counter;
}
Полиморфизм с forall
Перед объявлением или определением любой функции может быть указатель переменных типа forall
. Он имеет следующий синтаксис:
forall <comma_separated_type_variables_names> ->
где имя переменной типа может быть любым идентификатором. Обычно они именуются заглавными.
Например,
forall X, Y -> [Y, X] pair_swap([X, Y] pair) {
[X p1, Y p2] = pair;
return [p2, p1];
}
это функция, которая принимает кортеж длиной ровно 2, но со значениями любых типов (одиночная запись стека) в компонентах и меняет их местами.
pair_swap([2, 3])
вернет [3, 2]
, а pair_swap([1, [2, 3, 4]])
вернет [[2, 3, 4], 1]
.
В этом примере X
и Y
являются переменными типа. При вызове функции переменные типа заменяются фактическими типами, и выполняется код функции. Обратите внимание, что хотя функция является полиморфной, фактический код ассемблера для нее одинаков для каждой подстановки типа. Это достигается по сути полиморфизмом примитивов манипуляции стеком. В настоящее время другие формы полиморфизма (например, ad-hoc полиморфизм с классами типов) не поддерживаются.
Также стоит отметить, что ширина типа X
и Y
должна быть равна 1; то есть значения X
или Y
должны занимать одну запись в стеке. Так что вы на самом деле не можете вызвать функцию pair_swap
для кортежа типа [(int, int), int]
, потому что тип (int, int)
имеет ширину 2, то есть занимает 2 записи в стеке.