Язык RIDE: выражение, которое отвечает да или нет
Зачем это нужно
Solidity похож на JavaScript: циклы, классы, изменяемые переменные, всё привычное. RIDE — не похож ни на что из того, что было раньше в треке, и это сделано нарочно.
В RIDE нет циклов. Нет своих типов. Нет изменяемых переменных. Нет обращений наружу. Убрано ровно то, из-за чего стоимость выполнения программы нельзя предсказать заранее. Взамен появляется свойство, которого нет ни в Solidity, ни в чейнкоде Fabric: цену любого скрипта считает компилятор, до всякого запуска.
Эта глава — про язык как таковой: из чего состоит скрипт, чем let отличается от strict и как читать сообщения компилятора. Всё ниже — вывод настоящего компилятора RIDE версии 2.3.0.
Три директивы
Любой скрипт начинается с трёх строк:
{-# STDLIB_VERSION 6 #-}
{-# CONTENT_TYPE EXPRESSION #-}
{-# SCRIPT_TYPE ACCOUNT #-}
Самый простой скрипт, который что-то делает:
{-# STDLIB_VERSION 6 #-}
{-# CONTENT_TYPE EXPRESSION #-}
{-# SCRIPT_TYPE ACCOUNT #-}
sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
сложность: 182 | размер base64: 88
Он воспроизводит поведение обычного аккаунта: «пропускай, если подпись верна». Скрипт-выражение обязан вернуть Boolean — если вернуть что-то другое, компилятор скажет прямо:
Script should return boolean
Значения вместо переменных
let a = 2
let b = 3
a + b == 5
сложность: 2 | размер: 44
Слово let объявляет значение, а не переменную. Поменять его нельзя:
let a = 1
let a = 2
Compilation failed: [Value 'a' already defined in the scope in 85-111]
Строки складываются тем же плюсом:
let s = "IS" + "-21"
s == "IS-21"
Деление — целочисленное:
(7 / 2) == 3
сложность: 2
А дробного числа просто нет:
let x = 1.5
Parse error: illegal expression in 95-96
Компилятор не понял 1.5 не как «плохое число», а как непонятное выражение: точки между цифрами в языке не бывает вовсе. Отсюда правило, знакомое по предыдущим главам: деньги считают целыми, а запятую ставят при показе.
let ленив, strict — нет
Это место стоит запомнить особо: одно слово меняет цену скрипта в шестьдесят раз.
@Callable(i)
func f() = {
let dorogo = sigVerify(base58'', base58'', base58'')
[ IntegerEntry("x", 1) ]
}
сложность: 3
@Callable(i)
func f() = {
strict dorogo = sigVerify(base58'', base58'', base58'')
[ IntegerEntry("x", 1) ]
}
сложность: 184
Значение под let вычисляется только если его кто-то спросил. В обоих примерах dorogo дальше не используется — и в первом случае за него не платят вовсе.
Отсюда ловушка, на которой теряют деньги. Проверку под let легко «потерять»:
let proverka = if (ball < 2 || ball > 5) then throw("plohoy ball") else true
[ IntegerEntry(k, ball) ]
Проверка не выполнится: её никто не спросил. Всё, что должно упасть, объявляют через strict — или ставят прямо в выражение результата.
if только с else
let a = 1
if (a == 1) then true
Compilation failed: [expected a false branch in 116-116]
if в RIDE — не оператор ветвления, а выражение, которое обязано дать значение при любом исходе. Ветка else обязательна всегда.
if (1 == 1) then true else false
сложность: 1
match: разбор по типу
match tx {
case t: TransferTransaction => t.amount > 0
case d: DataTransaction => true
case _ => false
}
сложность: 2 | размер: 172
Уберём последнюю ветку:
Compilation failed: [Matching not exhaustive: possibleTypes are Order, ReissueTransaction,
BurnTransaction, MassTransferTransaction, ExchangeTransaction, TransferTransaction,
SetAssetScriptTransaction, InvokeScriptTransaction, … while matched are TransferTransaction]
Компилятор перечислил все типы, которые вы не разобрали. Это не придирка: пропущенная ветка в скрипте на аккаунте означает, что какой-то вид транзакции пройдёт без проверки.
Matching not exhaustive — редкий случай, когда сообщение компилятора длиннее самой ошибки. Зато читать его не надо: достаточно добавить case _ => и решить, что делать с остальными.
Чтение необязательных значений
Чтение из состояния возвращает «значение или ничего»:
let v = getInteger(this, "ball") // Int|Unit
Достать из него число можно тремя способами:
valueOrElse(getInteger(this, "ball"), 0) // подставить запасное
getIntegerValue(this, "ball") // упасть, если нет
match getInteger(this, "ball") { // разобрать вручную
case n: Int => n
case _ => 0
}
valueOrElse → сложность 13
getIntegerValue → сложность 11
getIntegerValue короче и дешевле, но падает на пустом ключе. Это ровно то, что нужно, когда отсутствие записи — ошибка: например, оценку ставят в несуществующую зачётку. А valueOrElse берут там, где пусто — законное состояние: например, вклад, которого ещё не было.
Сложность
Число, которое компилятор печатает рядом с размером, — сложность: счётчик работы скрипта. Каждая встроенная функция стоит фиксированно, цены сложены заранее.
a + b → 2
if (1 == 1) then true else false → 1
getInteger(this, "ball") → 11
sha256(…) → 120
sigVerify(…) → 182
Узел показывает сложность каждой функции dApp отдельно:
curl .../addresses/scriptInfo/<адрес>
"callableComplexities":{"polozhit":20,"zabrat":22,"skolko":14}
В основной сети сложность ограничена и влияет на комиссию: тяжёлый скрипт стоит дороже, а слишком тяжёлый не примут. На локальном узле ограничения смягчены — у нас установилась и выполнилась функция сложностью 14 483. Поэтому проверять укладываемость надо в той сети, где будет работать приложение, а не на своей машине.
Чего в языке нет
- Циклов. Бесконечный скрипт написать нельзя в принципе. Списки обрабатывают функциями
FOLD, у которых число шагов известно заранее. - Своих типов и классов. Структура данных известна компилятору целиком.
- Изменяемых переменных. Объявленное один раз значение не меняется.
- Дробных чисел. Деление целое:
7 / 2даёт3. - Обращений наружу. Скрипт не ходит в сеть и не читает файлы: неоткуда взяться расхождению между узлами.
Итог — то самое свойство, ради которого всё это сделано: цена скрипта известна до запуска.
Типичные ошибки
Ошибка 1. Проверка под let.
Разобрана выше. Самая дорогая ошибка в языке: код есть, а проверки нет.
Ошибка 2. if без else.
expected a false branch
Ошибка 3. Неполный match.
Matching not exhaustive: possibleTypes are Order, ReissueTransaction, …
Добавить case _ => и решить, что делать с остальными типами.
Ошибка 4. Опечатка в имени функции.
sigVerifu(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
Can't find a function 'sigVerifu'(ByteVector, ByteVector, ByteVector) or it is @Callable in 85-142
Обратите внимание: компилятор перечислил типы аргументов. Это же сообщение появится, если имя верное, а типы не те.
Ошибка 5. Присваивание вместо сравнения.
let x = 5
x = 6
Parse error: expected "let" or "strict" keyword in 95-96
Компилятор увидел строку, которая начинается с имени, и ждал объявления.
Шпаргалка по главе
| Что нужно | Как |
|---|---|
| Шапка скрипта | три директивы: STDLIB_VERSION, CONTENT_TYPE, SCRIPT_TYPE |
| Объявить значение | let a = 2 — ленивое |
| Заставить вычислить | strict a = … |
| Ветвление | if (…) then … else … — else обязателен |
| Разбор по типу | match x { case t: Type => … case _ => … } |
| Чтение с запасным значением | valueOrElse(getInteger(this, k), 0) |
| Чтение со строгим падением | getIntegerValue(this, k) |
| Скомпилировать | compile(src) из @waves/ride-js |
| Узнать сложность | поле complexity в результате компиляции |
Словарь главы
| Термин | Что это |
|---|---|
| Директива | Строка {-# … #-} в шапке; меняет правила разбора |
EXPRESSION | Скрипт-выражение: обязан вернуть Boolean |
DAPP | Скрипт-приложение с вызываемыми функциями |
let | Ленивое значение: считается только при обращении |
strict | Значение, которое вычисляется всегда |
| Сложность | Счётчик работы скрипта; считается компилятором заранее |
| `Int | Unit` |
Matching not exhaustive | В match разобраны не все возможные типы |
Цель: скорость ≥ 90 зн/мин, точность ≥ 88%
{-# STDLIB_VERSION 6 #-}
Что должен уметь после главы
- написать шапку скрипта и объяснить каждую директиву;
- отличить
letотstrictи не потерять проверку; - написать
ifиmatchтак, чтобы компилятор не ругался; - прочитать значение из состояния тремя способами и выбрать подходящий;
- объяснить, что такое сложность и откуда она берётся;
- назвать пять отсутствующих возможностей языка и объяснить, зачем их убрали;
- разобрать пять типичных сообщений компилятора.
Проверка по главе
Сводная проверка по всей главе: 10 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Документация RIDE — язык целиком на русском языке.
- Встроенные функции и их цена — таблица сложностей.
- Waves IDE — редактор с подсветкой и компиляцией прямо в браузере.
- @waves/ride-js — компилятор, которым получены все числа этой главы.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.