TON Telegram Open Network - Fift - часть 3 - функции-слова, константы, библиотеки

avatar cromlehg 1 month ago

Оглавление
Предыдущая статья
Следующая статья

Введение

В этой статье мы познакомимся с еще одним условным оператором. Узнаем как создавать функции или слова и константы. Научимся использовать библиотеки с готовыми функциями.

Функции или слова

Наш скрипт умеет выводить подсказку пользователя если кол-во агрументов не равно единице. Подсказка у нас общая и может выводится при любом неправильном использовании скрипта.
Поскольку мест отображения подсказки может быть несколько то и код печатающий подсказку будет везде дублироваться. Чтобы не дублировать код нужно вынести его в одно место. В функцию.
Как и большинство языков программирования в fift можно создавать функции.  Только называются они слова.   

По сути слова - это выражения которым присвоили имя. Давайте посмотрим как они определяются:

{ .. } : имя        

Вызывать функции очень просто - в месте, в котором хотите ее вызвать пишите название функции. А теперь давайте изменим наш скрипт таким образом, чтобы подсказку распечатывала функция.

{ ."Please, specify only your name!" cr } : usage

$# 1 = { ."Hello, " @' $1 type ."!" cr } usage cond

В нашей подсказке написано что нужно написать имя. Но пользователь не должен догадываться как и где написать это имя. Давайте уточним нашу подсказку и покажем как вызывать скрипт. Для этого изменим нашу подсказку следующим образом:

{ ."Usage: " @' $0 type ." <name>" cr } : usage

Из предыдущей статьи мы помним, что $0 - это всегда название скрипта. Итак, теперь подсказка будет выводить следующий текст:

Usage: nw.fif 

Отлично, теперь пользователь точно будет знать как пользоваться скриптом.

Условный оператор и выход из программы

Если пользователь передал нашему скрипту правильные аргументы, то начнем выполнять выражение приветсвия. В дальнейшем мы будем дописывать наш скрипт. И выражение в скобке будет разрастаться. Вскоре писать наш код в скобке будет не очень удобно и не читабельно. С другой стороны после показа сообщения об ошибке мы можем сразу завершить скрипт.

Давайте сначала выполним все проверки и завершим скрипт если проверки не прошли. А потом уже будем выполнять остальную часть сркипта. 

Для завершения скрипта есть оператор halt . Он берет из стэка код ошибки и выходит из программы возвращая код ошибки. Давайте добавим его в вывод подсказки с кодом ошибки 1:

{ ."Usage: " @' $0 type ." <name>" cr 1 halt } : usage

Теперь каждый раз после распечатки подсказки скрипт будет завершаться. Но как же нам избавиться от скобок в той части где приветствие? Для этого есть еще один условый оператор if и его противоположность ifnot. Оператор этот работает следующим образом - он забирает со стэка два последних значения. Последнее должно быть выражение, которое будет исполнено, если предпоследний аргумент на стэке не равен 0. Перепишем нашу проверку с этим оператором:

$# 1 = usage ifnot

Что тут происходит?

  1. $# на стэк помещается кол-во аргументов
  2. На стэк помещается единица
  3. Оператор = достает последние два значения со стэка. Если они равны, то помещает на стэк -1 если нет 0
  4. usage выполняет выражение, которое имеет соответствующее название
  5. ifnot - в стэке выполнится выражение только если предыдущее значение на стэке равно нулю.

Если Вы обратили внимание, то в 4-ом пункте функция usage выполнятся сразу. Т.е. написание названия выражения, как мы уже знаем, заставляет выполняться выражение. А нам для корректной работы ifnot нужно не выполнить выражение usage а поместить его стэк, чтобы ifnot его оттуда забрал! Для этого воспользуемся оператором '  <слово>- он помещает выражение которое обозначает слово на стэк. Теперь проверка выглядит так:

$# 1 = ' usage ifnot

Комментарии к скрипту пишутся после оператора // - c пробелом. Давайте посмотрим что у нас в итоге получилось:

// объявляем функцию которая выводит подсказку и выходит из программы
{ ."Usage: " @' $0 type ." <name>" cr 1 halt } : usage

// делаем проверку на кол-во агрументов и 
// если проверка не прошла, то выводим подсказку и выходим
$# 1 = ' usage ifnot

// после всех проверок выполняем основную часть программы 
."Hello, " @' $1 type ."!" cr

В итоге наш скрипт стал более структурированным. Мы всегда можем дописать еще одну проверку и переиспользовать функцию подсказки. И можем спокойно дописывать основную часть скрипта в конце, не задумываясь в какой части условия она написана как раньше.

Константы и функции с аргументами

Сейчас наш скрипт выводит только приветствие. Пусть наш скрипт делает что-нибудь полезное. Например, складывает грамы (моенты Gram) и отображает сумму в нанограмах. Для этого мы будем передавать скрипту два аргумента в грамах. Скрипт будет вычислять сумму. А потом переведет ее в нанограмы и отобразит.

Сначала перепишем подсказку:

{ ."Usage: " @' $0 type ." <value 1> <valut 2>" cr 1 halt } : usage

Поправим проверку так, чтобы она тестировала на два аргумента. Поскольку передавать мы будем два числа

$# 2 = ' usage ifnot

Добавим проверку, что первый и второй аргументы действительно являются числами, а не произвольными символами. Для этого воспользуемся оператором (number) . Работает он следующим образом:

  1. Мы помещаем на стэк строку
  2. (number) достает со стэка строку и пытается преобразовать в целое или дробное число. 
    1. Если число дробное, то помещает на стэк: знаменатель, числитель, 2. Таким образом дробное число представляется в виде числитель/знаменатель.
    2. Если число целое то помещает на стэк: число, 1.
    3. Если не удалось преобразовать то на стэк помещается 0.

Проиллюстрируем работу (number) для наглядности:

Итак, если число будет целым, что нам и нужно, то в стэке мы получим 1 и преобразованное из строки число. Поскольку чисел, которые нужно проверять у нас два, то лучше написать функцию. Эта функция будет пытаться преобразовать помещенную на стэк строку. Если не получится преобразовать то вызовет функцию с подсказкой. Если получиться то оставит на стэке преобразованное число. Назовем эту функцию parse-int-grams:

{ (number) 1 = ' usage ifnot } : parse-int-grams

С этой функцией мы можем дописать обе проверки аргументов, а заодно и сложить числа. Сложение выполняется оператором +, который достает со стэка два последних значения, складывает и помещает результат на стэк.:

$1 parse-int-grams $2 parse-int-grams +

Осталось только перевести сумму в нанограммы. 1 Gram = 1 000 000 000 nano Gram. Сколько нанограмов в граме - это постоянная величина. Поэтому мы можем ее для удобства определить как константу. Давайте посмотрим как это сделать на fift:

1000000000 constant Gram

Т.е. определение константы идет так:
<значение> constant <название константы>

Значение кладется на стэк, а constant достает его и объявляет как константу. Давайте определим функцию преобразования в нанограмы из грамов для удобства, возможно она нам еще пригодится в будущем:

{ Gram * } : Gram*

Думаю, вы уже догадались что оператор * забирает  два последних значения на стэке. Перемножает и помещает в стэк результат. Стоит отметить что операторы пишутся через пробелы. Так например в названии функции присутствует *. Но поскольку он не отделен от названия функции пробелом, то не будет рассматриваться как оператор. А будет рассматриваться как часть имени функции. Теперь давайте добавим в наш скрипт перевод в нанограмы и посмотрим что у нас получилось:

// объявляем функцию которая выводит подсказку и выходит из программы
{ ."Usage: " @' $0 type ." <value 1> <valut 2>" cr 1 halt } : usage

// объявляем функцию проверки и преобразования строки на стэке в число
{ (number) 1 = ' usage ifnot } : parse-int-grams

// объявим константу - сколько нанограмов в граме
1000000000 constant Gram

// объявим функцию перевода из грамов в нанограмы
{ Gram * } : Gram*

// делаем проверку на кол-во агрументов и 
// если проверка не прошла, то выводим подсказку и выходим
$# 2 = ' usage ifnot

// проверяем и преобразовываем аргументы, а затем 
// складываем их и переводим в нанограмы
$1 parse-int-grams $2 parse-int-grams + Gram*

// выводим результат
."Result: " . ." nanograms." cr

В выводе результата у нас появился оператор - отдельная точка - . 
Это оператор берет число со стэка и выводит на экран. Мы уже знаем два оператора вывода со стэка на экран:

  1. type - выводит строку
  2. . - выводит число

Библиотеки готовых функций

Во многих примерах скриптов на языке fift вы можете увидеть вначале такие конструкции:

#!/usr/bin/env fift -s
"TonUtil.fif" include
"Asm.fif" include

Первая строчка говорит командной оболочке (shell-подобной в Linux) какую программу запускать для исполнения скрипта. Тех кто хоть раз писал на скрипты на Linux знакомы с этой конструкцией. Для наших целей она мало полезна, потому что мы запускаем fift напрямую и передаем ему имя скрипта, библиотеки и аргументы. Но все же рекомендую эту строчку указывать.

Нам больше интересны вторая и третья строчки. Это команда включения библиотек функций:

"<библиотека>" include

В данном случае подключаются библиотеки TonUtil.fif и Asm.fif. Эти две библиотеки подключаются в большинстве скриптов из примеров TON. Ищет fift эти библиотеки в директории, которую мы указывали после параметра -I. В дальнейшем рекомендуется подключать обе эти библиотеки. Для чего они нужны?

  • Asm.fif  -  тут описаны команды, в которых описываются смарт-контракты. Грубо говоря это ассемблер, написаный на fift. Если Вы будете писать смарт-контракты на FunC, то компилятор будет транслировать сначала Ваш контракт в fift. Если Вы откроете сформированный таким образом fif файл, то увидите там как раз команды, описанные в библиотеке Asm.fif
    Стоит отметить что идея писать ассемблер на fift очень инетерсна. Таким образом, если мы захотим сделать возможность писать контракты на Solidity, то нам не нужно будет писать компилятор, который будет переводить Solidity в ассемблер виртуальной машины TON (в дальнейшем TVM - Ton Virtual Machine). Нам нужно написать ассемблер Solidity на fift и мы получим совместимость всех языков, которые работают с виртуальной машиной EVM (Ethereum Virtual Machine). Таким же образом мы можем написать ассемблер JVM (Java Virtual Machine) и писать контракты на Java или Scala. 
  • TonUtil.fif - Вспомогательные функции. Это могут быть различные проверки, чтения адресов, сохранения файлов. В дальнейшем мы очень часто будем пользоваться функциями оттуда.

В нашем контракте мы писали проверку что в аргументах передается число, а не произвольны символы. Подобная функция уже написана в TonUtil и мы ею могли воспользоваться. Вот как она выглядит:

{ (number) 1- abort"integer expected" } : parse-int

Общая логика простая. Но есть незнакомые нам операторы, поэтому прежде чем описать как работает эта функция, расскажем про два оператора:

  • число- - как ни странно, но это эквивалент записи: число - . Т.е. вычитает из помещенного на стэк числа указанное.
  • abort"текст" -   возвращает исключение с описанием ошибки если последнее число на стэке не равно нулю. Стоит отметить исключение не только печатает  строку и выходит из программы, но и выводится название файла в котором произошло исключение и номер строки. Не стоит путать с оператором abort . Который в отличи от первого вариант никаких проверок не делает а выводит исключение со строкой, коротую берет со стэка.

Давайте прокомментриуем работу функции parse-int:

  1. (number) - берет со стэка строку и пытается преобразовать в число. Если получилось целое число, то помещает на стэк 1. Если дробное то 2, если не получилось то 0.
  2. 1- - вычитает из верхушки стэка 1
  3. abort"integer expected" - проверяет что на стэке 0, если это не так то выводит исключение

Такми образом исключение не выведится если после (number) 1-  на стэке останется 0. А это возможно только если (number) поместит на стэк 1, т.е. если перед выполненеим (number) на стэке лажала строка с целым числом.

Также в TonUtils есть константа Gram и функция Gram*. И код у нее точно такой же как и у нас. Поэтому подключив TonUtils мы можем смело убирать определния этих контсант и фнукций. Давайте перепишем наш контракт с использованием функции parse-int и Gram* из библиотеки TonUtil:

#!/usr/bin/env fift -s
"TonUtil.fif" include
"Asm.fif" include

// объявляем функцию которая выводит подсказку и выходит из программы
{ ."Usage: " @' $0 type ." <value 1> <valut 2>" cr 1 halt } : usage

// делаем проверку на кол-во агрументов и 
// если проверка не прошла, то выводим подсказку и выходим
$# 2 = ' usage ifnot

// проверяем и преобразовываем аргументы, а затем 
// складываем их и переводим в нанограмы
$1 parse-int $2 parse-int + Gram*

// выводим результат
."Result: " . ." nanograms." cr

Отлично! Теперь мы умеем использовать библиотеки с готовыми функциями.

Резюме

В этой статье мы научились создавать функции и константы. Узнали новый условный оператор и использовали готовую функцию и стандартной библиотеки!

Обсудить, задать вопросы или сообщить о неточностях можно в нашей официальной группе - Blockchain's Witnesses

Оглавление
Предыдущая статья
Следующая статья