Solidity. Смарт контракты и аудит
2.62K subscribers
246 photos
7 videos
18 files
550 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
Погружение в Core Solidity. Часть 3

Обобщения и классы типов

Core Solidity вводит два новых механизма для повторного использования кода и полиморфизма: обобщения (generics) и классы типов (иногда также называемые трейтами, traits).

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

forall T . function identity(x : T) -> T {
return x;
}


Здесь forall вводит новую переменную-тип T, область видимости которой ограничена определением функции.

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

data Result(T) = Ok | Err(T)


Обобщения весьма мощны, но сами по себе довольно ограничены. Большинство интересных операций не определены для всех типов вообще. Классы типов решают эту проблему: они позволяют задавать перегруженные, специфичные для каждого типа реализации одной и той же сигнатуры функции. В сочетании с ограничениями классов типов они предоставляют возможность писать обобщённые функции, полиморфные лишь над ограниченным подмножеством типов.

Класс типов — это просто спецификация интерфейса. Рассмотрим, например, определение класса типов, которые поддерживают операцию умножения:

forall T . class T:Mul {
function mul(lhs : T, rhs : T) -> T;
}


Вместо конкретной функции wmul, которую мы определили выше для нашего типа wad с фиксированной точкой, более идиоматично создать экземпляр (в терминологии Rust — impl) класса типов Mul для wad. Это даёт единообразный синтаксис умножения для всех типов и позволяет использовать wad в функциях, обобщённых над любыми типами, реализующими Mul:

instance wad:Mul {
function mul(lhs : wad, rhs : wad) -> wad {
return wmul(lhs, rhs);
}
}


Если мы хотим написать функцию, принимающую любой тип, для которого определён экземпляр Mul, необходимо добавить ограничение в сигнатуру:

forall T . T:Mul => function square(val : T) -> T {
return Mul.mul(val, val);
}


Простые обёрточные типы вроде wad встречаются очень часто. Один из особенно полезных классов типов при работе с ними — Typedef:

forall T U . class T:Typedef(U) {
function abs(x : U) -> T;
function rep(x : T) -> U;
}


Функции abs (абстрагирование) и rep (представление) позволяют единообразно преобразовывать обёрточные типы во внутренние и наоборот, избегая синтаксического шума, связанного с необходимостью использовать сопоставление с образцом каждый раз при распаковке значения. Экземпляр для wad выглядел бы так:

instance wad:Typedef(uint256) {
function abs(u : uint256) -> wad {
return wad(u);
}

function rep(x : wad) -> uint256 {
match x {
| wad(u) => return u;
}
}
}


Обратите внимание: параметры, следующие после имени класса (например, U в определении Typedef выше), являются «слабыми» — их значение однозначно определяется значением параметра T. Если вы знакомы с Haskell или Rust, то это по сути ассоциированный тип (associated type) (хотя, для тех, кто разбирается в системах типов, реализовано это с помощью ограниченной формы функциональных зависимостей). Проще говоря, для wad можно определить только один экземпляр Typedef: компилятор не разрешит одновременно объявить и wad:Typedef(uint256), и wad:Typedef(uint128). Это ограничение делает вывод типов значительно более предсказуемым и надёжным, избегая многих неоднозначностей, присущих полноценным многопараметрическим классам типов.
2