Link Search Menu Expand Document

Generics - dynamic types

Swift Rust

pending


supported


Generics can refer to dynamic types with constraints, with type safety guarantees provided by the runtime.

As described in “Generics - static types”, we can define generic algorithms that can operate on concrete types. This page explores the considerations of using generalized types without knowing the underlying concrete types.

The main concern of using generic types dynamically is to ensure the same degree of type safety at runtime as there is during compilation.

This is usually achieved by a pattern called “boxing/unboxing”. In this pattern, a type is “boxed” to allow referring to its generic type, and “unboxed” to safely refer to its concrete type.

Swift

In Swift, boxed types are also called “existential types” and are referred to by their protocol name.

For a long time, Swift had several limitations for existential uses. These limitations would frequently be surprising to developers and have limited the usefulness of the feature. Oftentimes, workarounds such as type erasing structs would be needed, such as AnyHashable and AnyPublisher. Other times, it would be impossible to write code that is truly generic at all.

Swift 5.7 has lifted most of the limitations, by implementing several big enhancements:

  • SE-0309 - Unlock existentials for all protocols - allow using a protocol as a type without limitations.
  • SE-0353 - Constrained Existential Types - allow constraining a dynamic type using its type parameters.
  • SE-0352 - Implicitly Opened Existentials - allow “unboxing” a dynamic type to a conrete type.
  • SE-0335 - Introduce existential any - a new keyword to explicitly refer to a protocol as a type. Introduced in Swift 5.6, will be mandatory for all protocols in Swift 6.

The enhancements to any types (existential types) work seamlessly with the enhancements to some types (opaque types).

To adapt the example from static types (Swift 5.7):

// define a generic type
protocol Costume {
    associatedtype BellType
    var bells: [BellType] { get }
}
// define a generic function that checks a concrete type
func hasBells(_ costume: some Costume) -> Bool {
    !costume.bells.isEmpty
}
// define a generic function that works with dynamic types inside an array
func firstWithBells(costumes: some Sequence<any Costume>) -> (any Costume)? {
    costumes.first(where: { hasBells($0) })
}

Note: constraining the associated type Sequence.Element with type parameter syntax Sequence<Element> is made possible by SE-0346 (Lightweight same-type requirements for primary associated types, Swift 5.7) and SE-0358 (Primary Associated Types in the Standard Library, in review).

This generic function can accept any Sequence that can contain different types of Costumes:

// define two concrete types conforming to `Costume`
struct GymnastCostume: Costume {
    let name: String
    var bells: [Never] { [] }
}
struct ClownCostume: Costume {
    enum ClownBell { case big, small }
    let name: String
    let bells: [ClownBell]
}
// a generic array of costumes
// the explicit type annotation is there so the array is not inferred to be `[Any]`
let costumesArray: [any Costume] = [
    GymnastCostume(name: "A"),
    ClownCostume(name: "B", bells: [.big, .big, .small])
]
// a concrete result `Optional<Costume>`
let firstCostume = firstWithBells(costumes: costumesArray)
print(firstCostume) // prints "B"

Rust

Rust also allows using generalized types by their interfaces. The term for this is “trait object” and it’s denoted using the dyn keyword.

Trait objects must be references, an as such are usually declared as:

  • &dyn Trait
  • Box<dyn Trait>

To implement a generalized function that finds the first costume with bells:

// define a generic type
pub trait Costume {
    fn description(&self) -> String;
    fn has_bells(&self) -> bool;
}
// define a generic dynamic function
fn first_with_bells<'a, 'b, I>(iter: &'a mut I) -> Option<Box<dyn Costume + 'b>>
where I: Iterator<Item = Box<dyn Costume + 'b>> {
    iter.find(|c| c.has_bells())
}

Use it with two distinct types:

// first concrete type
struct GymnastCostume {
    name: &'static str,
    has_bells: bool,
}
impl Costume for GymnastCostume {
    fn description(&self) -> String {
        let name = self.name;
        format!("Gymnast {name}").to_string()
    }
    fn has_bells(&self) -> bool {
        self.has_bells
    }
}
// second concrete type
struct ClownCostume {
    name: &'static str,
}
impl Costume for ClownCostume {
    fn description(&self) -> String {
        let name = self.name;
        format!("Clown {name}").to_string()
    }
    fn has_bells(&self) -> bool {
        true
    }
}
// use it
fn main() {
    // heterogenous vector
    let costumes_vec: Vec<Box<dyn Costume>> = vec![
        Box::new(GymnastCostume { name: "A", has_bells: false }),
        Box::new(ClownCostume { name: "B" }),
    ];
    let mut iter = costumes_vec.into_iter();
    let first_costume = first_with_bells(&mut iter);
    println!("{:?}", first_costume.unwrap().description()); // prints "Clown B"
}