# Начало работы
# Введение
Ride — лаконичный и дружественный язык для разработки смарт-контрактов и децентрализованных приложений (dApps) на блокчейне Waves. В нем устранены многие серьезные недостатки других популярных языков смарт-контрактов.
Этот раздел содержит введение в Ride, примеры, описание дополнительных инструментов и ресурсов. Его изучение займет около часа.
# Общие сведения
Ride — это компилируемый, функциональный, статически типизированный язык программирования на основе выражений. Он является неполным по Тьюрингу, поскольку не имеет циклов (итерации можно реализовать с помощью макроса FOLD<N>
, см. ниже). Благодаря этому сложность скрипта известна заранее и комиссия за выполнение предсказуема.
Несмотря на простой синтаксис, Ride предоставляет множество возможностей разработчикам. Он во многом похож на Scala и отчасти на F#.
# “Hello world!”
Начнем с базового примера:
func say() = {
"Hello world!"
}
Для объявления функций в Ride используется ключевое слово func
(см. ниже). Тип возвращаемого значения автоматически определяется компилятором, и объявлять его не нужно. В приведенном выше примере say
возвращает строку Hello World!
. В языке нет оператора return
, потому что Ride основан на выражениях (всё является выражением), а последний оператор является результатом функции.
# Блокчейн
Ride разработан для выполнения на блокчейне и оптимизирован для этой цели. Поскольку блокчейн — это распределенный реестр, который хранится на множестве серверов по всему миру, функции Ride не могут обратиться к файловой системе или отобразить что-либо в консоли. Вместо этого функции Ride могут читать данные из блокчейна и выполнять действия на блокчейне.
# Комментарии
Комментарии в Ride похожи на комментарии в Python:
# Это комментарий
# Многострочные комментарии не предусмотрены
"Hello world!" # Комментировать можно и так
# Директивы
Каждый скрипт на Ride должен начинаться с директив для компилятора. Предусмотрено три типа директив с различными возможными значениями.
{-# STDLIB_VERSION 8 #-}
{-# CONTENT_TYPE DAPP #-}
{-# SCRIPT_TYPE ACCOUNT #-}
STDLIB_VERSION
задает версию стандартной библиотеки. Последняя версия, доступная в Mainnet, — 8.
CONTENT_TYPE
определяет содержание скрипта:
- Тип
DAPP
позволяет объявлять функции и в завершение скрипта выполнять действия, в результате которых изменяются балансы аккаунтов, свойства ассетов, а также записи в хранилище данных dApp. - Тип
EXPRESSION
представляет собой логическое выражение и используется для валидации транзакций.
SCRIPT_TYPE
определяет тип объекта, к которому прикреплен скрипт: ACCOUNT
или ASSET
.
Не все комбинации директив допустимы. Следующий пример не будет работать, поскольку тип содержания DAPP
допустим только для аккаунтов. Тип EXPRESSION
применим как для аккаунтов, так и ассетов.
{-# STDLIB_VERSION 8 #-}
{-# CONTENT_TYPE DAPP #-}
{-# SCRIPT_TYPE ASSET #-} # тип содержания DAPP недопустим для ассетов
# Переменные
Для объявления переменных используется ключевое слово let
.
let a = "Bob"
let b = 1
Значения переменных в Ride недоступны для изменения.
Ride строго типизирован, а тип переменной определяется исходя из значения.
Ride позволяет вам определять переменные глобально, внутри любой функции или даже внутри определения переменной.
func lazyIsGood() = {
let a = "Bob"
let b = {
let x = 1
"Alice"
}
true
}
Функция, определенная выше, возвращает значение true
, но переменная a
не будет инициализирована, поскольку инициализация с помощью let
ленивая: значения неиспользуемых переменных не вычисляются. Для нетерпеливой инициализации используется ключевое слово strict
.
# Функции
Функции в Ride можно использовать только после их объявления.
func greet(name: String) = {
"Hello, " + name
}
func add(a: Int, b: Int) = {
func m(a:Int) = a
m(a) + b
}
Тип аргумента (Int
, String
) указывается после имени.
Как и во многих других языках, функции не могут быть перегружены. Это помогает сохранить код удобным для понимания и поддержки.
func calc() = {
42
}
func do() = {
let a = calc()
true
}
Функция calс
не будет вызвана, так как переменная a
не используется.
В отличие от большинства языков, переопределение переменных не допускается. Объявление переменной с именем, которое уже используется в родительской области видимости, приведет к ошибке компиляции.
Вызовы функции могут быть префиксные или постфиксные:
let list = [1, 2, 3]
let a1 = list.size()
let a2 = size(list)
let b1 = getInteger(this, "key")
let b2 = this.getInteger("key")
В этом примере a1
— то же самое, что и a2
, а b1
— то же самое, что и b2
.
# Базовые типы
Основные базовые типы:
Boolean # true
String # "Hey"
Int # 1610
ByteVector # base58'...', base64'...', base16'...', fromBase58String("...") и т. д.
Мы рассмотрим строки и специальные типы.
# Строки
let name = "Bob" # используйте только "двойные" кавычки
let coolName = name + " is cool!" # для конкатенации строк используется знак +
name.indexOf("o") # 1
Как и другие типы данных в Ride, строки недоступны для изменения. Поэтому функция substring
очень эффективна: не нужно ни копировать данные, ни выделять дополнительную память.
В строковых данных используется кодировка UTF-8. Для обозначения строк используйте только двойные кавычки.
С обеих сторон оператора в Ride должны быть указаны значения одного типа. Следующий код не компилируется, потому что age
— целое число:
let age = 21
"Bob is " + age # не компилируется
Чтобы исправить это, нужно преобразовать age
в строку:
let age = 21
"Alice is " + age.toString() # работает!
# Специальные типы
В Ride есть несколько основных типов, которые работают так же, как в Scala.
# Unit
В Ride нет типа null
, как во многих других языках. Многие встроенные функции возвращают значение типа unit
вместо null
.
"String".indexOf("substring") == unit # true
# Ничто
Ничто — тривиальный тип в системе типов Ride. Ни одно значение не может быть типа «ничто», но выражение с типом «ничто» можно использовать где угодно. В функциональных языках это необходимо для поддержки исключений:
2 + throw() # это выражение компилируется,
# поскольку определена функция +(Int, Int).
# Тип второго операнда — «ничто»,
# которое совместимо с любым другим типом.
# Список
let list = [16, 10, 1997, "birthday"] # может содержать данные различных типов
let second = list[1] # 10 — второе значение в списке
У списков нет полей, но функции и операторы Cтандартной библиотеки упрощают работу с ними.
let list = [16, 10, 1997, "birthday"]
let last = list[(list.size() - 1)] # "birthday", постфиксный вызов функции size()
let lastAgain = getElement(list, size(list) - 1) # то же самое
Функция .size()
возвращает длину списка. Обратите внимание: это значение доступно только для чтения и не может быть изменено. Кстати, last
может быть разного типа: тип определяется только после того, как вычислено значение.
let initList = [16, 10] # начальное значение
let newList = cons(1997, initList) # [1997, 16, 10]
let newList2 = 1997 :: initList # [1997, 16, 10]
let newList2 = initList :+ 1 # [16, 10, 1]
let newList2 = [4, 8, 15, 16] ++ [23, 42] # [4 8 15 16 23 42]
- Чтобы добавить элемент в начало списка, используйте функцию
cons
или оператор::
. - Чтобы добавить элемент в конец списка, используйте оператор
:+
. - Чтобы объединить два списка, используйте оператор
++
.
# Кортеж
Кортеж — упорядоченный набор элементов любого типа.
let x=("Hello Waves",42,true)
let num = x._2 # 42
let (a,b,c) = x
let bool = c # true
# Union-типы. Сравнение типов
let valueFromBlockchain = getString("3PHHD7dsVqBFnZfUuDPLwbayJiQudQJ9Ngf", "someKey") # Union(String | Unit)
Union-типы — это удобный способ работы с абстракциями. Union(String | Unit)
означает, что результат представляет собой пресечение этих типов.
Простой пример (пожалуйста, имейте в виду, что определение пользовательских типов будет поддержано в следующих версиях Ride):
type Human : { firstName: String, lastName: String, age: Int}
type Cat : {name: String, age: Int }
Union(Human | Cat)
— объект с одним полем age
:
Human | Cat => { age: Int }
Сравнение типов:
let t = ... # Cat | Human
t.age # OK
t.name # Ошибка компиляции
let name = match t { # OK
case h: Human => h.firstName
case c: Cat => c.name
}
Механизм сравнения типов используется для работы с транзакциями:
let amount = match tx { # tx — исходящая транзакция
case t: TransferTransaction => t.amount
case m: MassTransferTransaction => m.totalAmount
case _ => 0
}
В Waves есть несколько типов транзакций, и в зависимости от типа количество переводимых токенов может быть указано в разных полях. Для транзакций перевода и массового перевода используется значение соответствующего поля, а в остальных случаях — 0.
# Функции чтения данных
let readOrZero = match getInteger(this, "someKey") { # чтение данных
case a:Int => a
case _ => 0
}
readOrZero + 1
getString
возвращает Union(String | Unit)
, поскольку при чтении данных блокчейна (записей в виде ключ-значение в хранилищах данных аккаунтов) некоторые пары ключ-значение могут не существовать.
let v = getInteger("3PHHD7dsVqBFnZfUuDPLwbayJiQudQJ9Ngf", "someKey")
v + 1 # приведет к ошибке компиляции, нужно предусмотреть
# возможность отсутствия значения по этому ключу
v.valueOrErrorMessage("oops") + 1 # компилируется и выполняется
let realStringValue2 = getStringValue(this, "someKey")
Чтобы получить реальный тип и значение из Union-типа, используйте функцию value
, которая прервет выполнение скрипта в случае значения unit
. Другой вариант — используйте специализированные функции, такие как getStringValue
, getIntegerValue
и др.
# If
let amount = 1610
if (amount > 42) then "Сумма больше 42"
else if (amount > 100500) then "Сумма слишком большая"
else "Что-то еще"
Инструкция if
довольно проста и похожа на большинство других языков, с двумя исключениями: if
— это выражение (результат можно присвоить переменной), поэтому инструкция else
обязательна.
let a = 16
let result = if (a > 0) then a / 10 else 0
# Исключения
throw("Here is exception text")
Фнкция throw
прерывает выполнение скрипта немедленно, с указанным текстом. Возможность перехватывать и обрабатывать исключения отсутствует. Идея в том, чтобы остановить выполнение и предоставить полезную обратную связь пользователю.
let a = 12
if (a != 100) then
throw ("a is not 100, actual value is " + a.toString())
else throw("A is 100")
# Предопределенные структуры данных
#LET THE HOLY WAR BEGIN
В Ride есть много предопределенных структур данных, характерных для блокчейна Waves, например: Address
, Alias
, Invocation
, Issue
, Lease
, ScriptTransfer
, StringEntry
, ExchangeTransaction
, SetScriptTransactions
.
let keyValuePair = StringEntry("someKey", "someStringValue")
Например, StringEntry
— это структура, которая описывает запись со строковым значением, например, для хранилища данных аккаунта.
Все структуры данных могут использоваться для сравнения типов, а также как конструкторы.
# Итерации с макросом FOLD<N>
Поскольку в виртуальной машине Ride не предусмотрены циклы, они реализованы на уровне компилятора с помощью макроса FOLD<N>
. Этот макрос ведет себя как функция свертки fold
в других языках программирования, принимая на вход количество итераций, начальные значения и сворачиваемую функцию.
Важный момент: N
задает максимальное количество выполняемых итераций. Это необходимо для поддержания предсказуемой стоимости вычислений.
Следующий код подсчитывает сумму числе в массиве:
let a = [1, 2, 3, 4, 5]
func foldFunc(acc: Int, e: Int) = acc + e
FOLD<5>(a, 0, foldFunc) # Результат: 15
FOLD<N>
также может использоваться для фильтрации и преобразования данных. Вот пример инвертирования списка:
let a = [1, 2, 3, 4, 5]
func foldFunc(acc: List[Int], e: Int) = (e + 1) :: acc
FOLD<5>(a, [], foldFunc) # Результат: [6, 5, 4, 3, 2]
# Аннотации
Функции могут быть объявлены без аннотаций либо с аннотацией @Callable
или @Verifier
. Функции с аннотациями используются только в скриптах с типом DAPP
.
{-# STDLIB_VERSION 8 #-}
{-# CONTENT_TYPE DAPP #-}
{-# SCRIPT_TYPE ACCOUNT #-}
func getPayment(i: Invocation) = {
if (size(i.payments) == 0)
then throw("Payment must be attached")
else {
let pmt = i.payments[0]
if (isDefined(pmt.assetId))
then throw("This function accepts WAVES tokens only")
else pmt.amount
}
}
@Callable(i)
func pay() = {
let amount = getPayment(i)
(
[
IntegerEntry(toBase58String(i.caller.bytes), amount)
],
unit
)
}
Аннотации могут привязывать к функции некоторые значения. В примере выше переменная i
привязана к функции pay
и хранит некоторые поля вызова функции: публичный ключ и адрес аккаунта, вызвавшего функцию; платежи, прикрепленные к вызову; комиссия; идентификатор транзакции и др.
Функции без аннотаций недоступны извне. Вызвать их можно только из других функций скрипта.
# Функция-верификатор
@Verifier(tx)
func verifier() = {
match tx {
case ttx: TransferTransaction => ttx.amount <= 100 # можно отправить до 100 токенов
case _ => false
}
}
Функция с аннотацией @Verifier
устанавливает правила валидации исходящих транзакций (dApp). Функция верификации не может быть вызвана извне, однако она выполняется при каждой попытке отправить транзакцию с аккаунта dApp.
Функция верификации должна возвращать логическое значение: разрешено отправить транзакцию в блокчейн или нет.
Скрипты-выражения с директивой {-# CONTENT_TYPE EXPRESSION #-}
и функции верификации с аннотацией @Verifier
должны возвращать только логические значения. В зависимости от этого значения транзакция будет принята (если true
) или отклонена (если false
).
@Verifier(tx)
func verifier() = {
sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
}
С функцией верификации связана переменная tx
, которая представляет собой объект с полями текущей исходящей транзакции.
В dApp-скрипте может быть только одна функция верификации.
# Вызываемая функция
Функция с аннотацией @Callable
может быть вызвана c других аккаунтов: с помощью транзакции вызова скрипта или из другого dApp.
Вызываемая функция может выполнять действия: записывать данные в хранилище данных dApp, переводить токены с аккаунта dApp других адресатам, выпускать/довыпускать/сжигать токены и т.д. Результат вызываемой функции — это кортеж из двух элементов: списка структур, описывающих действия скрипта, и значения, которое в случае вызова dApp из dApp передается исходному dApp.
@Callable(i)
func giveAway(age: Int) = {
(
[
ScriptTransfer(i.caller, age, unit),
IntegerEntry(toBase58String(i.caller.bytes), age)
],
unit
)
}
Каждый аккаунт, вызвавший функцию giveAway
, получит столько WAVELET, сколько ему лет. Структура ScriptTransfer
задает параметры перевода токена. Кроме того, dApp сохранит информацию об этом в своем хранилище данных. Параметры целочисленной записи — ключ и значение — задает структура IntegerEntry
.
# Тестирование и инструменты
Вы можете опробовать Ride в REPL как онлайн на https://waves-ide.com/, так и через терминал с surfboard
:
> npm i -g @waves/surfboard
> surfboard repl
Для дальнейшей разработки полезны следующие инструменты и утилиты:
- Плагин Visual Studio Code: waves-ride
- Инструмент командной строки для компиляции и тестирования
surfboard
: https://github.com/wavesplatform/surfboard - Онлайн IDE с примерами: https://waves-ide.com/
# Отличной работы!
Надеемся, эта статья дала вам хорошее введение в Ride — простой, безопасный, мощный язык программирования для смарт-контрактов и dApps на блокчейне Waves.
Теперь вы готовы писать свои собственные смарт-контракты, и у вас есть инструменты для их тестирования перед развертыванием на блокчейне Waves.
Если вам нужна помощь в изучении основ языка Ride, вы можете изучить уроки Waves.