Функции высшего порядка и анонимные функции
Функции обладают статусом «первоклассных» объектов в системе типов, что позволяет использовать их в качестве параметров, возвращаемых значений и присваиваемых сущностей.
В качестве примера рассмотрим следующий фрагмент, реализующий пользовательскую декодировку ABI для тройки логических значений из одного слова:
forall T . function unpack_bools(fn : (bool, bool, bool) -> T) -> ((word) -> T) {
return lam (bools : word) -> {
let wordToBool = lam (w : word) { return w > 0; };
// extract the right-most bit from `bools`
let b0 = wordToBool(and(bools, 0x1));
// shift `bools` by one and extract the right-most bit
let b1 = wordToBool(and(shr(1, bools), 0x1));
// shift `bools` by two and extract the right-most bit
let b2 = wordToBool(and(shr(2, bools), 0x1));
return fn(b0, b1, b2);
};
}Функция unpack_bools реализует пользовательскую декодировку ABI. Она является функцией высшего порядка, которая «оборачивает» входную функцию, принимающую три отдельных логических значения и возвращающую значение произвольного типа T, извлекая аргументы из трёх младших битов входного слова. Такой пример невозможно реализовать в классическом Solidity, даже с использованием модификаторов, поскольку они не могут изменять аргументы, передаваемые внутрь оборачиваемой функции.
Кроме того, поддерживается определение (некурсивных) анонимных функций с помощью ключевого слова lam. Функции, определённые таким образом, могут захватывать значения из области видимости, в которой они объявлены. В качестве примера рассмотрим вспомогательную функцию для тестирования, подсчитывающую количество вызовов произвольной функции:
forall T U . function count_calls(fn : (T) -> U) -> (memory(word), (T) -> U) {
let counter : memory(word) = allocate(32);
return (counter, lam (a : T) -> {
counter += 1;
return fn(a);
});
}Реализация здесь аналогична тому, как это сделано в системных языках, таких как Rust и C++: компилятор генерирует уникальный тип для каждой анонимной функции, содержащей захваченные значения, а эти уникальные типы становятся вызываемыми за счёт принадлежности к специальному типовому классу «вызываемых» (invokable), подобно трейту Fn в Rust. Такой подход обеспечивает высокую эффективность с точки зрения затрат газа во время выполнения.
Вывод типов
Core Solidity поддерживает вывод типов почти в любом контексте. Аннотации типов обычно требуются только тогда, когда это желательно для улучшения читаемости или понимания кода. Алгоритм вывода типов разрешим, а ситуации, в которых возникает неоднозначность и требуется явная аннотация, крайне ограничены. Благодаря этому удаётся избавиться от большого количества синтаксического шума, присущего классическому Solidity.
Например, присваивание выражения переменной в классическом Solidity нередко приводит к избыточным аннотациям, даже если типы уже присутствуют в самом выражении:
(bytes memory a, bytes memory b) = abi.decode(input, (bytes, bytes));
То же самое определение в Core Solidity выглядит значительно чище:
let (a, b) = abi.decode(input, (uint256, uint256));
Ещё одна частая причина раздражения при работе с классическим Solidity — синтаксический шум при определении литералов массивов. Рассмотрим следующий фрагмент:
uint256[3] memory a = [1, 2, 3];
Это объявление отвергается компилятором классического Solidity со следующей ошибкой:
Error: Type uint8[3] memory is not implicitly convertible to expected type uint256[3] memory.
Причина ошибки заключается в том, что классический Solidity реализует ограниченную и специализированную форму вывода типов для литералов массивов: тип элементов массива определяется как тип первого выражения в списке, к которому можно неявно привести все остальные элементы (в данном случае — uint8). Затем компилятор выдаёт ошибку несоответствия типов при попытке присвоить это значение переменной несовместимого типа.