Generics
Learn about generics in Move, which enable code reuse across different types.
Generics
Generics enable defining functions and structs that work with multiple data types while maintaining type safety. This feature, also known as parametric polymorphism, allows you to write reusable code that operates on any type satisfying specified constraints.
Key benefits:
- Code reuse: Write once, use with multiple types
- Type safety: Compile-time type checking prevents runtime errors
- Library development: Essential for building flexible, reusable components
Generics are extensively used in Move's standard library (like vector<T>) and are crucial for building type-safe, reusable smart contract components.
Type Parameters
Both functions and structs can accept type parameters enclosed in angle brackets <...>. These parameters act as placeholders for concrete types that will be specified later.
Generic Functions
Type parameters are placed after the function name and before the parameter list:
module 0x42::utils {
// Generic identity function - works with any type
fun id<T>(x: T): T {
x
}
// Generic swap function
fun swap<T>(x: T, y: T): (T, T) {
(y, x)
}
}Generic Structs
Type parameters follow the struct name and can be used in field definitions:
module 0x42::containers {
struct Box<T> has copy, drop {
value: T
}
struct Pair<T1, T2> has copy, drop {
first: T1,
second: T2,
}
}Using Generics
Function Calls
You can explicitly specify type arguments or let Move's type inference determine them:
fun example() {
// Explicit type specification
let x = id<bool>(true);
// Type inference (preferred when possible)
let y = id(42u64); // T inferred as u64
}Struct Construction
Similar to functions, type arguments can be explicit or inferred:
fun create_containers() {
// Explicit types
let box = Box<u64> { value: 100 };
// Type inference
let pair = Pair { first: true, second: 42 }; // Pair<bool, u64>
}Type Inference
Move's compiler automatically infers types in most cases, reducing verbosity while maintaining safety:
fun inference_examples() {
let numbers = vector[1, 2, 3]; // vector<u64> inferred
vector::push_back(&mut numbers, 4); // T inferred from usage
}When manual annotation is required:
- Functions with type parameters only in return positions
- Ambiguous contexts where multiple types are possible
Phantom Type Parameters
Phantom type parameters solve a critical problem with ability derivation in generic structs. When a struct has generic parameters, it can only have abilities that all its type parameters possess - even if those parameters aren't actually used in the struct fields.
The Problem
Consider this currency system without phantom parameters:
module 0x42::currency {
struct Currency1 {} // No abilities
struct Currency2 {} // No abilities
// This struct wants 'store' ability
struct Coin<Currency> has store {
value: u64
}
}Issue: Even though Currency1 and Currency2 are never used in Coin's fields, the struct Coin<Currency1> cannot have the store ability because Currency1 lacks it. This prevents storing coins in global storage!
The Solution: Phantom Parameters
Phantom parameters are excluded from ability derivation, solving this problem:
module 0x42::currency {
struct Currency1 {} // Still no abilities needed
struct Currency2 {}
// phantom means Currency doesn't affect abilities
struct Coin<phantom Currency> has store {
value: u64
}
public fun mint<Currency>(amount: u64): Coin<Currency> {
Coin { value: amount } // Now Coin<Currency1> has 'store'!
}
}Declaration Rules
Phantom parameters can only appear in "phantom positions":
// Valid: T1 not used at all
struct S1<phantom T1, T2> { f: u64 }
// Valid: T1 only used as phantom argument
struct S2<phantom T1, T2> { f: S1<T1, T2> }
// Invalid: T used in non-phantom position
struct S3<phantom T> { f: T }
// Invalid: T used as argument to non-phantom parameter
struct S4<phantom T> { f: vector<T> }Key benefits:
- Ability independence: Phantom parameters don't affect struct abilities
- Type safety: Different phantom arguments create distinct types
- Zero runtime cost: No storage or computation overhead
Constraints
By default, generic type parameters have no abilities, making the type system very conservative. Constraints allow you to specify what abilities unknown types must have, enabling safe operations that would otherwise be forbidden.
Why Constraints Are Needed
Without constraints, generic functions are severely limited:
fun unsafe_consume<T>(x: T) {
// error! x does not have 'drop'
}
fun unsafe_double<T>(x: T) {
(copy x, x)
// error! x does not have 'copy'
}Declaring Constraints
Constraints specify required abilities using the : syntax:
// Single constraint
fun consume<T: drop>(x: T) {
// valid!
// x will be dropped automatically
}
// Multiple constraints
fun double<T: copy>(x: T) {
(copy x, x) // valid!
}
// All four abilities
T: copy + drop + store + keyConstraint Verification
Constraints are checked at call sites, not definitions:
struct R {}
fun foo() {
let r = R {};
consume<R>(r);
// ^ error! R does not have 'drop'
}
fun foo(): (R, R) {
let r = R {};
double<R>(r)
// ^ error! R does not have 'copy'
}Practical Examples
module 0x42::safe_operations {
// Safe resource cleanup
fun cleanup<T: drop>(items: vector<T>) {
// All items automatically dropped
}
// Safe value duplication
fun backup<T: copy>(original: T): (T, T) {
(copy original, original)
}
// Safe global storage
struct Container<T: store> has key {
contents: T
}
}Unused Type Parameters
Move allows type parameters that don't appear in struct fields, enabling powerful type-level programming. These "unused" parameters provide compile-time type distinctions without runtime overhead.
Type-Level Distinctions
Unused parameters create different types that are structurally identical but logically distinct:
module 0x42::currency_system {
// Currency specifier types (empty structs)
struct USD {}
struct EUR {}
struct BTC {}
// Generic coin - Currency parameter is "unused"
struct Coin<Currency> has store {
value: u64 // Currency doesn't appear here!
}
// Each currency creates a distinct type
public fun mint_usd(value: u64): Coin<USD> {
Coin { value }
}
public fun mint_eur(value: u64): Coin<EUR> {
Coin { value }
}
// Type safety: can't mix currencies!
public fun exchange_usd_to_eur(usd: Coin<USD>): Coin<EUR> {
let Coin { value } = usd;
Coin<EUR> { value: value * 85 / 100 } // 85% exchange rate
}
}Benefits of Unused Parameters
Type Safety: Prevents mixing incompatible values:
fun example() {
let dollars = mint_usd(100);
let euros = mint_eur(85);
// Compile error: type mismatch!
// exchange_usd_to_eur(euros); // EUR ≠ USD
// Correct usage
let converted = exchange_usd_to_eur(dollars);
}Generic Programming: Write code that works with any currency:
// Generic function works with any currency
public fun get_value<Currency>(coin: &Coin<Currency>): u64 {
coin.value
}
// Specific function for USD only
public fun get_usd_value(coin: &Coin<USD>): u64 {
coin.value
}Real-World Applications
module 0x42::access_control {
// Permission types
struct AdminRole {}
struct UserRole {}
struct GuestRole {}
// Capability with role-based access
struct Capability<Role> has store {
permissions: u64
}
// Only admins can create admin capabilities
public fun create_admin_cap(): Capability<AdminRole> {
Capability { permissions: 0xFF }
}
// Role-specific operations
public fun admin_only<T>(_cap: &Capability<AdminRole>, data: T): T {
data // Only callable with admin capability
}
}Key advantages:
- Zero runtime cost: No extra storage or computation
- Compile-time safety: Type errors caught early
- Clear intent: Types express domain concepts
- Flexible design: Easy to extend with new "categories"
Limitations
Recursive Restrictions
Move prevents certain recursive patterns to ensure type system soundness:
// Direct recursion not allowed
struct Node<T> {
value: T,
next: Node<T> // ERROR: recursive struct
}
// Infinite type generation not allowed
fun recursive_types<T>() {
recursive_types<vector<T>>(); // ERROR: infinite types
}Type-Level Recursion
The compiler conservatively prevents patterns that could generate infinite types, even if they would terminate at runtime:
fun controlled_recursion<T>(depth: u64) {
if (depth > 0) {
controlled_recursion<vector<T>>(depth - 1); // ERROR: still forbidden
}
}Best Practices
- Use descriptive type parameter names (
CurrencyvsT) - Prefer type inference over explicit specification
- Use phantom parameters for type-level distinctions
- Apply minimal necessary constraints
Summary
Generics enable writing reusable, type-safe code that works across multiple data types:
- Code reuse: Write once, use with multiple types while maintaining type safety
- Type parameters: Functions and structs accept placeholders for concrete types
- Constraints: Specify required abilities for safe operations on unknown types
- Phantom parameters: Enable type-level programming without affecting abilities
- Unused parameters: Create type distinctions without runtime overhead
- Type inference: Compiler automatically determines types in most cases
- Zero runtime cost: All generic resolution happens at compile time
- Essential for libraries: Critical for building robust, reusable Move components