πŸ¦€ Const Generics

December 6, 2024 by CryptoPatrick programming rust

Goldmoon - The 3rd Level Magician

It’s Friday night. You and a group of friends are playing the role-playing game, Dungeons & Dragons. Your character, Goldmoon, is a 3rd level Magic User. Goldmoon, like any magician, has the power to cast magic spells.

Limitations

The number of spells that she can memorize is limited. The power of her spells is also constrained by her level (she has attained 3rd level magic user). Higher level, more powerful, spells demand more preparation and memory than lower level spells.

According to the official Dungeons & Dragons rulebook, Goldmoon, at her level (3rd) can memorize the following number of spells per level:

  • 1st-level spells: 4
  • 2nd-level spells: 2

This means that Goldmoon cannot memorize and cast —, a 3rd-level spell. She’s simply not powerful enough, yet.

Once Goldmoon reaches the 4th level, her spell casting abilities will grow to look like this:

  • 1st-level spells: 4
  • 2nd-level spells: 2
enum FirstLevelSpells {
  CharmPerson,
  DetectMagic,
  FloatingDisc,
  HoldPortal,
  Light,
  MagicMissile,
  ProtectionFromEvil,
  ReadLanguages,
  ReadMagic,
  Shield,
}

enum SecondLevelSpells {
  ContinualLight,
  DetectEvil,
  DetectInvisible,
  ESP,
  Invisibility,
  Knock,
  Levitate,
  LocateObject,
  MirrorImage,
  PhantasmalForce,
  Web,
  WizardLock,
}

struct SpellBook {
    spells: Vec<[FirstLevel; const N: usize], [SecondLevel; const N: usize]>
}

fn main() {
    let gooldmoon
    let mut memorize_spells = Vec<[FirstLevel; 4], [SecondLevel, 2]>
}

To cast a 1st-level spell like Magic Missile, the wizard uses one 1st-level slot. For a 2nd-level spell like Mirror Image, a 2nd-level slot is used.


Array of Pain

Handling arrays in Rust can be somewhat painful. The reason is:

  1. First, arrays are fixed-length: once declared, the length of an array cannot be changed.
  2. Second, arrays are typed based on their length and the type of their elements (which can only be of one type).

Arrays were only considered to be of the same type if they had both the same size and the same type of element. Before const generics, working with arrays in Rust could be painful.

Const Generics to the rescue

Const Generics, which dropped in Stable version 1.83, enable us to define a generic over a constant value, like the length of an array. This is useful because quivers are fixed-size arrays.

There are three types of generics in πŸ¦€ Rust: LifetimeParam, TypeParam, and ConstParam.

let quiver = [WoodenArrow; 10];
let quiver = [WoodenArrow; 11];
let quiver = [SilverTipArrow; 10];

Here’s our first problem. But like any container, the capacity (size) of the quiver can vary. We want to create a generic trait that will apply to any arrow quiver, regardless of its capacity.

// Only array_a and array_d are of the same type.
struct ElfArcher {
    array_a: [u8; 5],
    array_b: [u8; 6], // larger than array_a, so different type
    array_c: [i8; 5], // elements of different type, so different type
    array_d: [u8; 5], // same type as array_a
}
// The keyword `const` below is what we use to inform the Rust compiler that we
// want to use a const generic for N (which is of type usize).
// Note! We have to use usize because Rust uses it to index arrays.
struct Warrior<T, const N: usize> {
    array_a: [T; N],
    array_b: [T; N],
}

Trait Pains

What if we want to implement a trait for these arrays? With const generics, we can now implement traits that work across arrays of different sizes, as long as they contain the same element type.

const generics can be used for other things but solving the previous pain of working with arrays was the main contribution.

'I write to understand as much as to be understood.' β€”Elie Wiesel
(c) 2024 CryptoPatrick