Associated types vs Generic type parameters in Rust and Substrate
Introduction
It's important to understand the concepts of associated types and generic type parameters when working with Rust and Substrate. These features provide flexibility and reusability in code, allowing for clear associations between traits and their associated or generic types. In this guide, we go through what associated types and generic type parameters are, the difference between them, where/how they're used in Substrate, and points to keep in mind when using them.
Help us measure our progress and improve Substrate in Bits content by filling out our living feedback form. Thank you!
Associated types
Associated types define types that are associated with a particular trait. The concrete types for these types are specified when implementing the trait for a type. Associated types are used where you expect that there will only be one implementation of the trait for a given type.
Here's an example:
pub trait Block {
type Hash: Hash;
type Transaction: Transaction;
// Other associated types and methods...
}
pub trait Transaction {
type Sender: Account;
type Recipient: Account;
// Other associated types and methods...
}
pub trait Account {
// Methods and associated types for account management...
}
pub struct DeBlock;
impl Block for DeBlock {
type Hash = [u8; 32];
type Transaction = MyTransaction;
// Implementations for other associated types and methods...
}
pub struct DeTransaction;
impl Transaction for DeTransaction {
type Sender = MyAccount;
type Recipient = MyAccount;
// Implementations for other associated types and methods...
}
pub struct MyAccount;
impl Account for MyAccount {
// Implementations for account management methods and associated types...
}
The example above has the definition of 3 traits:
- The
Block
trait, which has theHash
andTransaction
associated types - The
Transaction
trait which has theSender
andRecipient
associated types - The
Account
trait for account management.
The associated types used here ensure that custom chains (ie, different implementations) are able to decide on their own definition of what each of these types should be. In the example above, the Deblock
, DeTransaction
, and MyAccount
types were created and the Block
, Transaction
, and Account
traits were implemented for them with the definition of concrete types.
Associated types are suitable for the Block
, Transaction
, and Account
traits because these traits represent concepts where it is not expected to have multiple instances of their associated types.
For example, a blockchain typically has a single type that represents blocks. By using associated types, we can define these specific types directly within the trait, providing a clear and concise representation of the expected associations.
Generic type parameters
Generic type parameters are a powerful feature that allows you to write generic code that can be reused with different types. They provide flexibility and enable you to write more generic and reusable functions, structs, and traits. Generic type parameters are used when it's plausible to have multiple implementations of the trait for a given type.
Here's an example:
pub trait Formatter<T> {
fn format(&self, value: T) -> String;
}
pub struct StringFormatter;
impl Formatter<String> for StringFormatter {
fn format(&self, value: String) -> String {
value
}
}
pub struct IntegerFormatter;
impl Formatter<i32> for IntegerFormatter {
fn format(&self, value: i32) -> String {
value.to_string()
}
}
pub struct CustomType {
// Implementation details...
}
pub struct CustomTypeFormatter;
impl Formatter<CustomType> for CustomTypeFormatter {
fn format(&self, value: CustomType) -> String {
// Custom formatting logic for CustomType
// Implementation details...
}
}
In this example, we have a Formatter
trait with a generic type parameter T. The trait defines a single method format that takes a value of type T and returns a formatted string.
We then have multiple implementations of the Formatter trait for different types. The StringFormatter implements Formatter for the String type, allowing us to format strings. The IntegerFormatter implements Formatter for the i32 type, enabling us to format integers. Finally, the CustomTypeFormatter implements Formatter for a custom type CustomType, allowing us to define custom formatting logic specific to that type.
By using generic type parameters, we can have multiple implementations of the Formatter trait for different types. Each implementation can provide its own specific formatting logic tailored to the given type. This flexibility allows us to reuse the Formatter trait with various types and have different formatting behaviors based on the type being formatted.
In this example, the use of generic type parameters makes sense because we expect to have multiple implementations of the Formatter trait for different types, enabling us to format values in a type-specific manner.
Essentially, generic type parameters are more suitable when you anticipate having multiple implementations of a trait for different types. On the other hand, associated types are more suitable when you expect that there will only be one implementation of the trait for a given type.
Where they are used in substrate
Let's take a look at the code snippets below:
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type representing the weight of this pallet
type WeightInfo: WeightInfo;
}
pub type MagicNumber<T> = StorageValue<_, u32>;
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::do_something())]
pub fn set_magic_number(who: T::AccountId, value: u32) {
//function body
}
}
The Pallet
struct is defined with the generic type parameter T
, which represents the configuration trait of the runtime.
By using the generic type parameter T
, the Pallet
struct can be instantiated with different configurations of the Substrate runtime. This allows the storage item to work with different types of account IDs, depending on the specific configuration used. Notice that the set_magic_number
has the generic type T::AccountId
as a parameter, allowing the storage item to handle different types of account IDs based on the runtime configuration being used.
By representing the runtime as a generic type parameter T
, the Pallet struct becomes adaptable and can be used with different runtimes that have varying configurations, as long as those runtimes implement the Config trait defined for the pallet.
The Config
trait, as shown above, defines two associated types: RuntimeEvent
and WeightInfo
. These associated types are not represented as parameter types because it is not expected to have multiple types of RuntimeEvent
or WeightInfo
for a particular pallet within the same runtime. Instead, the associated types provide a way to associate specific types with the pallet's methods or associated functions, ensuring a clear and concise representation of the expected associations.
Avoiding errors
Here're a couple of things to keep in mind in order to avoid errors when working with associated types and generics in substrate
- Ensure that all associated types required by the trait are implemented in your runtime
- Be mindful of the constraints placed on generic type parameters. Ensure that the constraints are appropriate and necessary for the desired functionality. Avoid overly restrictive constraints that may limit the usability of the code.
- Thoroughly test code that uses associated types and generic type parameters. Write unit tests that cover different scenarios and edge cases to ensure that the code behaves as expected and handles various inputs correctly.
Summary
This guide discusses the usage of associated types and generic type parameters in Substrate. Associated types are used when you expect only one implementation of a trait for a given type, while generic type parameters are used when you anticipate having multiple implementations of a trait for different types.
The guide also provides examples of how associated types and generic type parameters are used in Substrate, such as defining traits with associated types and implementing them for specific types.
Help us measure our progress and improve Substrate in Bits content by filling out our living feedback form. Thank you!